1450 lines
38 KiB
JavaScript
1450 lines
38 KiB
JavaScript
const COMCIP_ORIGIN = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com";
|
|
const COMCIP_APP_URL =
|
|
`${COMCIP_ORIGIN}/ic/builder/rt/oalset_semc/live/webApps/Dashboard/?page=shell&shell=main&main=service-requests-detailed-view`;
|
|
const COMCIP_QUERY_URL =
|
|
`${COMCIP_ORIGIN}/ic/builder/rt/oalset_semc/live;profile=PROD/services/auth/1.1/proxy/oalsetCRMRestAPIElastic/uri/https/eeho.fa.us2.oraclecloud.com/crmRestApi/searchResources/latest/custom-actions/queries`;
|
|
const COMCIP_CLIENT_ID_PROBE_URL =
|
|
`${COMCIP_ORIGIN}/ic/builder/rt/oalset_semc/live;profile=PROD/services/auth/1.1/proxy/oalsetSeaaSOKECustomRestAPI/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/crm/countries`;
|
|
const COMCIP_TIME_ENTRY_APP_URL =
|
|
`${COMCIP_ORIGIN}/ic/builder/rt/oalset_timeentrymobile/live/webApps/timeentry/`;
|
|
const COMCIP_TIME_ENTRY_CLIENT_ID_PROBE_URL =
|
|
`${COMCIP_ORIGIN}/ic/builder/rt/oalset_timeentrymobile/live;profile=PROD/services/auth/1.1/proxy/oke_seaas/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/crm/lookups?lookupType=SCTA_TASK_TYPE&type=CUSTOM`;
|
|
const COMCIP_TIME_ENTRY_APP_VERSION = "version_1774016034953";
|
|
const COMCIP_TAB_URL_PATTERN =
|
|
`${COMCIP_ORIGIN}/ic/builder/rt/oalset_semc/live*`;
|
|
const COMCIP_TIME_ENTRY_TAB_URL_PATTERN =
|
|
`${COMCIP_ORIGIN}/ic/builder/rt/oalset_timeentrymobile/live*`;
|
|
const SPA_WORKBENCH_FRAME_URL =
|
|
"https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html?ojr=workload_workbench";
|
|
const SPA_WORKBENCH_FRAME_ID = "arch-panel-extension-spa-frame";
|
|
const SPA_WORKBENCH_FRAME_HOST_ID = "arch-panel-extension-spa-frame-host";
|
|
const SPA_FRAME_DNR_RULE_ID = 92001;
|
|
|
|
void installSpaFrameHeaderRules();
|
|
|
|
chrome.runtime.onInstalled.addListener(() => {
|
|
void installSpaFrameHeaderRules();
|
|
});
|
|
|
|
chrome.runtime.onStartup.addListener(() => {
|
|
void installSpaFrameHeaderRules();
|
|
});
|
|
|
|
async function installSpaFrameHeaderRules() {
|
|
if (!chrome.declarativeNetRequest?.updateDynamicRules) {
|
|
return;
|
|
}
|
|
|
|
await chrome.declarativeNetRequest.updateDynamicRules({
|
|
removeRuleIds: [SPA_FRAME_DNR_RULE_ID],
|
|
addRules: [
|
|
{
|
|
id: SPA_FRAME_DNR_RULE_ID,
|
|
priority: 1,
|
|
action: {
|
|
type: "modifyHeaders",
|
|
responseHeaders: [
|
|
{
|
|
header: "x-frame-options",
|
|
operation: "remove",
|
|
},
|
|
{
|
|
header: "frame-options",
|
|
operation: "remove",
|
|
},
|
|
{
|
|
header: "content-security-policy",
|
|
operation: "remove",
|
|
},
|
|
{
|
|
header: "content-security-policy-report-only",
|
|
operation: "remove",
|
|
},
|
|
],
|
|
},
|
|
condition: {
|
|
initiatorDomains: ["eeho.fa.us2.oraclecloud.com"],
|
|
requestDomains: ["spa.oracle.com"],
|
|
resourceTypes: ["sub_frame"],
|
|
urlFilter: "||spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html",
|
|
},
|
|
},
|
|
],
|
|
}).catch(() => {});
|
|
}
|
|
|
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
if (message?.type === "ARCH_PANEL_SPA_FRAME_FETCH") {
|
|
executeSpaFrameFetch({
|
|
senderTabId: sender?.tab?.id,
|
|
url: message.url,
|
|
options: message.options || {},
|
|
})
|
|
.then(sendResponse)
|
|
.catch((error) => {
|
|
sendResponse({
|
|
ok: false,
|
|
status: 0,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
});
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
if (message?.type === "ARCH_PANEL_EXTENSION_FETCH") {
|
|
executeExtensionFetch(message.url, message.options || {})
|
|
.then(sendResponse)
|
|
.catch((error) => {
|
|
sendResponse({
|
|
ok: false,
|
|
status: 0,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
});
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
if (
|
|
!message ||
|
|
(message.type !== "ARCH_PANEL_COMCIP_POST" &&
|
|
message.type !== "ARCH_PANEL_COMCIP_REQUEST")
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
requestComcipFromPage({
|
|
url: message.url || COMCIP_QUERY_URL,
|
|
method: message.method || "POST",
|
|
payload: message.payload,
|
|
headers: message.headers,
|
|
senderTabId: sender?.tab?.id,
|
|
useTimeEntryFrame: Boolean(message.useTimeEntryFrame),
|
|
})
|
|
.then(sendResponse)
|
|
.catch((error) => {
|
|
sendResponse({
|
|
ok: false,
|
|
status: 0,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
});
|
|
});
|
|
|
|
return true;
|
|
});
|
|
|
|
async function executeExtensionFetch(requestUrl, options = {}) {
|
|
const method = options.method || "GET";
|
|
const headers = {
|
|
...(options.headers || {}),
|
|
};
|
|
const fetchOptions = {
|
|
method,
|
|
credentials: "include",
|
|
cache: "no-store",
|
|
headers,
|
|
};
|
|
|
|
if (options.body !== undefined && options.body !== null) {
|
|
fetchOptions.body = options.body;
|
|
}
|
|
|
|
const response = await fetch(requestUrl, fetchOptions);
|
|
const text = await response.text();
|
|
|
|
return {
|
|
ok: response.ok,
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
headers: pickExtensionResponseHeaders(response.headers),
|
|
payload: parseExtensionResponseText(text),
|
|
debug: {
|
|
url: requestUrl,
|
|
headerKeys: Object.keys(headers),
|
|
extensionFetch: true,
|
|
},
|
|
};
|
|
}
|
|
|
|
async function executeSpaFrameFetch({ senderTabId, url, options = {} }) {
|
|
if (!senderTabId) {
|
|
throw new Error("Unable to locate the current tab for the SPA frame request.");
|
|
}
|
|
|
|
await installSpaFrameHeaderRules();
|
|
await ensureSpaFrameElementInTab(senderTabId);
|
|
await waitForSpaFramePresent(senderTabId);
|
|
await ensureSpaBridgeInFrames(senderTabId);
|
|
await waitForSpaFrameCapturedAuth(senderTabId, url);
|
|
|
|
return executeSpaFrameScriptFetch(senderTabId, url, options);
|
|
}
|
|
|
|
async function ensureSpaFrameElementInTab(tabId, options = {}) {
|
|
await chrome.scripting.executeScript({
|
|
target: { tabId, frameIds: [0] },
|
|
world: "MAIN",
|
|
args: [
|
|
SPA_WORKBENCH_FRAME_HOST_ID,
|
|
SPA_WORKBENCH_FRAME_ID,
|
|
SPA_WORKBENCH_FRAME_URL,
|
|
Boolean(options.forceReload),
|
|
],
|
|
func: (hostId, frameId, frameUrl, forceReload) => {
|
|
const desiredParent = document.body || document.documentElement;
|
|
|
|
if (!desiredParent) {
|
|
return false;
|
|
}
|
|
|
|
let host = document.getElementById(hostId);
|
|
|
|
if (!host) {
|
|
host = document.createElement("div");
|
|
host.id = hostId;
|
|
host.setAttribute("aria-hidden", "true");
|
|
}
|
|
|
|
Object.assign(host.style, {
|
|
position: "fixed",
|
|
width: "1440px",
|
|
height: "1000px",
|
|
overflow: "hidden",
|
|
opacity: "0",
|
|
pointerEvents: "none",
|
|
border: "0",
|
|
left: "-20000px",
|
|
top: "0",
|
|
zIndex: "0",
|
|
});
|
|
|
|
if (host.parentElement !== desiredParent) {
|
|
desiredParent.appendChild(host);
|
|
}
|
|
|
|
let frame = document.getElementById(frameId);
|
|
|
|
if (!(frame instanceof HTMLIFrameElement)) {
|
|
frame = document.createElement("iframe");
|
|
frame.id = frameId;
|
|
frame.name = frameId;
|
|
frame.setAttribute("aria-hidden", "true");
|
|
frame.setAttribute("referrerpolicy", "no-referrer-when-downgrade");
|
|
frame.tabIndex = -1;
|
|
}
|
|
|
|
frame.name = frameId;
|
|
frame.dataset.archPanelFrame = "spa-workbench";
|
|
Object.assign(frame.style, {
|
|
position: "absolute",
|
|
width: "1440px",
|
|
height: "1000px",
|
|
opacity: "0",
|
|
pointerEvents: "none",
|
|
border: "0",
|
|
left: "0",
|
|
top: "0",
|
|
});
|
|
|
|
if (frame.parentElement !== host) {
|
|
host.appendChild(frame);
|
|
}
|
|
|
|
if (
|
|
forceReload ||
|
|
!frame.src ||
|
|
frame.src === "about:blank" ||
|
|
frame.src !== frameUrl
|
|
) {
|
|
if (forceReload) {
|
|
frame.src = "about:blank";
|
|
}
|
|
|
|
frame.src = frameUrl;
|
|
}
|
|
|
|
return true;
|
|
},
|
|
}).catch(() => {});
|
|
}
|
|
|
|
async function waitForSpaFramePresent(tabId) {
|
|
const startedAt = Date.now();
|
|
let lastFrames = "";
|
|
let lastEnsureAt = 0;
|
|
let forceReloaded = false;
|
|
|
|
while (Date.now() - startedAt < 45000) {
|
|
const elapsed = Date.now() - startedAt;
|
|
|
|
if (Date.now() - lastEnsureAt > 2500) {
|
|
await ensureSpaFrameElementInTab(tabId, {
|
|
forceReload: elapsed > 7000 && !forceReloaded,
|
|
});
|
|
lastEnsureAt = Date.now();
|
|
|
|
if (elapsed > 7000) {
|
|
forceReloaded = true;
|
|
}
|
|
}
|
|
|
|
const frames = await getFrameHints(tabId).catch(() => []);
|
|
const spaFrame = frames.find((frame) => isSpaFrameHint(frame));
|
|
|
|
lastFrames = frames
|
|
.map((frame) => `${frame.frameId}:${frame.url || frame.name || "blank"}`)
|
|
.slice(0, 8)
|
|
.join(" | ");
|
|
|
|
if (spaFrame) {
|
|
return spaFrame;
|
|
}
|
|
|
|
await delay(500);
|
|
}
|
|
|
|
throw new Error(
|
|
`SPA Workbench iframe was not found.${lastFrames ? ` Frames: ${lastFrames}` : ""}`
|
|
);
|
|
}
|
|
|
|
async function getFrameHints(tabId) {
|
|
const injectionResults = await chrome.scripting.executeScript({
|
|
target: { tabId, allFrames: true },
|
|
world: "MAIN",
|
|
func: () => ({
|
|
href: String(window.location.href || ""),
|
|
name: String(window.name || ""),
|
|
}),
|
|
});
|
|
|
|
return (injectionResults || []).map((item) => ({
|
|
frameId: item.frameId,
|
|
url: item.result?.href || "",
|
|
name: item.result?.name || "",
|
|
}));
|
|
}
|
|
|
|
function isSpaFrameHint(frame) {
|
|
const url = String(frame?.url || "");
|
|
const name = String(frame?.name || "");
|
|
|
|
return (
|
|
url.startsWith("https://spa.oracle.com/") ||
|
|
name === "arch-panel-extension-spa-frame"
|
|
);
|
|
}
|
|
|
|
async function ensureSpaBridgeInFrames(tabId) {
|
|
await chrome.scripting.executeScript({
|
|
target: { tabId, allFrames: true },
|
|
world: "MAIN",
|
|
files: ["page-bridge.js"],
|
|
}).catch(() => {});
|
|
}
|
|
|
|
async function waitForSpaFrameCapturedAuth(tabId, requestUrl) {
|
|
const startedAt = Date.now();
|
|
let lastHeaderKeys = "";
|
|
let lastSnapshot = null;
|
|
const requiresWwbToken = String(requestUrl || "").includes("/provider-proxy/wwb-provider/");
|
|
|
|
while (Date.now() - startedAt < 90000) {
|
|
await ensureSpaBridgeInFrames(tabId);
|
|
const snapshot = await readSpaFrameCapturedHeaders(tabId, requestUrl).catch(() => null);
|
|
const headerKeys = Object.keys(snapshot || {});
|
|
|
|
lastSnapshot = snapshot;
|
|
lastHeaderKeys = headerKeys.join(", ");
|
|
|
|
if (requiresWwbToken && snapshot?.["wwb-provider-authorization"]) {
|
|
return snapshot;
|
|
}
|
|
|
|
if (
|
|
!requiresWwbToken &&
|
|
(snapshot?.["wwb-provider-authorization"] ||
|
|
snapshot?.["spa-ts-authorization"] ||
|
|
snapshot?.["authorization"])
|
|
) {
|
|
return snapshot;
|
|
}
|
|
|
|
await delay(1000);
|
|
}
|
|
|
|
return lastSnapshot || {};
|
|
}
|
|
|
|
async function readSpaFrameCapturedHeaders(tabId, requestUrl) {
|
|
const injectionResults = await chrome.scripting.executeScript({
|
|
target: { tabId, allFrames: true },
|
|
world: "MAIN",
|
|
args: [requestUrl],
|
|
func: (url) => {
|
|
const href = String(window.location.href || "");
|
|
const frameName = String(window.name || "");
|
|
const isSpaFrame =
|
|
href.startsWith("https://spa.oracle.com/") ||
|
|
frameName === "arch-panel-extension-spa-frame";
|
|
|
|
if (!isSpaFrame) {
|
|
return {
|
|
matched: false,
|
|
};
|
|
}
|
|
|
|
return {
|
|
matched: true,
|
|
headers:
|
|
window.__archPanelExtensionBridge?.buildHeaderSnapshot?.({
|
|
url,
|
|
includeAllAuth: true,
|
|
}) || {},
|
|
};
|
|
},
|
|
});
|
|
|
|
const matchedResult = (injectionResults || [])
|
|
.map((item) => item.result)
|
|
.find((result) => result?.matched);
|
|
|
|
return matchedResult?.headers || null;
|
|
}
|
|
|
|
async function executeSpaFrameScriptFetch(tabId, requestUrl, options = {}) {
|
|
const startedAt = Date.now();
|
|
let lastError = "";
|
|
|
|
while (Date.now() - startedAt < 45000) {
|
|
await ensureSpaBridgeInFrames(tabId);
|
|
const response = await executeSpaFrameScriptFetchOnce(tabId, requestUrl, options).catch(
|
|
(error) => {
|
|
lastError = error instanceof Error ? error.message : String(error);
|
|
return null;
|
|
}
|
|
);
|
|
|
|
if (response?.matched) {
|
|
return response;
|
|
}
|
|
|
|
await delay(700);
|
|
}
|
|
|
|
throw new Error(
|
|
`SPA Workbench frame did not return a result.${lastError ? ` ${lastError}` : ""}`
|
|
);
|
|
}
|
|
|
|
async function executeSpaFrameScriptFetchOnce(tabId, requestUrl, options = {}) {
|
|
const injectionResults = await chrome.scripting.executeScript({
|
|
target: { tabId, allFrames: true },
|
|
world: "MAIN",
|
|
args: [
|
|
requestUrl,
|
|
options?.method || "GET",
|
|
options?.body ?? null,
|
|
options?.headers || {},
|
|
],
|
|
func: async (requestUrl, requestMethod, requestBody, sourceHeaders) => {
|
|
function isSpaFrame() {
|
|
const href = String(window.location.href || "");
|
|
const frameName = String(window.name || "");
|
|
|
|
return (
|
|
href.startsWith("https://spa.oracle.com/") ||
|
|
frameName === "arch-panel-extension-spa-frame"
|
|
);
|
|
}
|
|
|
|
if (!isSpaFrame()) {
|
|
return {
|
|
matched: false,
|
|
frameUrl: window.location.href,
|
|
frameName: window.name || "",
|
|
};
|
|
}
|
|
|
|
const bridge = window.__archPanelExtensionBridge;
|
|
|
|
if (bridge?.fetchWithCapturedHeaders) {
|
|
const response = await bridge.fetchWithCapturedHeaders(requestUrl, {
|
|
method: requestMethod || "GET",
|
|
headers: sourceHeaders || {},
|
|
body: requestBody,
|
|
});
|
|
|
|
return {
|
|
matched: true,
|
|
...response,
|
|
debug: {
|
|
...(response.debug || {}),
|
|
frameUrl: window.location.href,
|
|
frameName: window.name || "",
|
|
spaFrameFetch: true,
|
|
},
|
|
};
|
|
}
|
|
|
|
function parseResponseText(text) {
|
|
if (!text) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
return JSON.parse(text);
|
|
} catch {
|
|
return text;
|
|
}
|
|
}
|
|
|
|
function pickResponseHeaders(responseHeaders) {
|
|
const safeHeaders = {};
|
|
const exposedHeaders = ["content-type", "x-request-id", "x-oracle-dms-ecid"];
|
|
|
|
for (const key of exposedHeaders) {
|
|
const value = responseHeaders.get(key);
|
|
|
|
if (value) {
|
|
safeHeaders[key] = value;
|
|
}
|
|
}
|
|
|
|
return safeHeaders;
|
|
}
|
|
|
|
function normalizeHeaders(headers) {
|
|
const normalized = {};
|
|
|
|
for (const [key, value] of Object.entries(headers || {})) {
|
|
const normalizedKey = String(key || "").toLowerCase();
|
|
|
|
if (!normalizedKey || value === undefined || value === null || value === "") {
|
|
continue;
|
|
}
|
|
|
|
normalized[normalizedKey] = String(value);
|
|
}
|
|
|
|
normalized.accept = normalized.accept || "application/json, text/plain, */*";
|
|
normalized["x-requested-with"] =
|
|
normalized["x-requested-with"] || "XMLHttpRequest";
|
|
|
|
return normalized;
|
|
}
|
|
|
|
const headers = normalizeHeaders(sourceHeaders);
|
|
const normalizedMethod = String(requestMethod || "GET").toUpperCase();
|
|
const fetchOptions = {
|
|
method: normalizedMethod,
|
|
credentials: "include",
|
|
cache: "no-store",
|
|
headers,
|
|
};
|
|
|
|
if (normalizedMethod !== "GET" && normalizedMethod !== "HEAD") {
|
|
if (!headers["content-type"]) {
|
|
headers["content-type"] = "application/json";
|
|
}
|
|
|
|
fetchOptions.body = requestBody;
|
|
}
|
|
|
|
const response = await fetch(requestUrl, fetchOptions);
|
|
const text = await response.text();
|
|
|
|
return {
|
|
matched: true,
|
|
ok: response.ok,
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
headers: pickResponseHeaders(response.headers),
|
|
payload: parseResponseText(text),
|
|
debug: {
|
|
url: requestUrl,
|
|
frameUrl: window.location.href,
|
|
frameName: window.name || "",
|
|
headerKeys: Object.keys(headers),
|
|
spaFrameFetch: true,
|
|
},
|
|
};
|
|
},
|
|
});
|
|
|
|
return (injectionResults || [])
|
|
.map((item) => item.result)
|
|
.find((result) => result?.matched) || null;
|
|
}
|
|
|
|
function stripGenericAuthHeaders(headers = {}) {
|
|
const nextHeaders = {};
|
|
|
|
for (const [key, value] of Object.entries(headers || {})) {
|
|
const normalizedKey = String(key || "").toLowerCase();
|
|
|
|
if (
|
|
normalizedKey === "authorization" ||
|
|
normalizedKey === "x-id-token" ||
|
|
normalizedKey === "spa-ts-authorization"
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
nextHeaders[key] = value;
|
|
}
|
|
|
|
return nextHeaders;
|
|
}
|
|
|
|
function parseExtensionResponseText(text) {
|
|
if (!text) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
return JSON.parse(text);
|
|
} catch {
|
|
return text;
|
|
}
|
|
}
|
|
|
|
function pickExtensionResponseHeaders(responseHeaders) {
|
|
const safeHeaders = {};
|
|
const exposedHeaders = ["content-type", "x-request-id", "x-oracle-dms-ecid"];
|
|
|
|
for (const key of exposedHeaders) {
|
|
const value = responseHeaders.get(key);
|
|
|
|
if (value) {
|
|
safeHeaders[key] = value;
|
|
}
|
|
}
|
|
|
|
return safeHeaders;
|
|
}
|
|
|
|
async function requestComcipFromPage({
|
|
url,
|
|
method = "POST",
|
|
payload,
|
|
headers = {},
|
|
senderTabId,
|
|
useTimeEntryFrame = false,
|
|
}) {
|
|
const route = getComcipRoute(url);
|
|
|
|
if (route.useSenderFrame || useTimeEntryFrame) {
|
|
if (!senderTabId) {
|
|
throw new Error("Unable to locate the current dashboard tab for COMCIP Time Entry.");
|
|
}
|
|
|
|
await waitForComcipFrameSessionReady(senderTabId, headers, route);
|
|
let response = await executeComcipFetchFromFrame(
|
|
senderTabId,
|
|
url,
|
|
method,
|
|
payload,
|
|
headers,
|
|
route
|
|
);
|
|
|
|
if (isAuthorizationFailure(response) || isOutdatedApplicationFailure(response)) {
|
|
await delay(1500);
|
|
await waitForComcipFrameSessionReady(senderTabId, headers, route);
|
|
response = await executeComcipFetchFromFrame(
|
|
senderTabId,
|
|
url,
|
|
method,
|
|
payload,
|
|
headers,
|
|
route
|
|
);
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
const activeTab = await captureActiveTab();
|
|
const target = await getComcipTab(route, activeTab);
|
|
|
|
try {
|
|
await restoreActiveTab(activeTab);
|
|
await waitForComcipTabReady(target.tab.id);
|
|
await waitForComcipSessionReady(target.tab.id, headers, route);
|
|
|
|
let response = await executeComcipFetch(
|
|
target.tab.id,
|
|
url,
|
|
method,
|
|
payload,
|
|
headers,
|
|
route
|
|
);
|
|
|
|
if (isAuthorizationFailure(response)) {
|
|
await delay(1500);
|
|
await waitForComcipSessionReady(target.tab.id, headers, route);
|
|
response = await executeComcipFetch(
|
|
target.tab.id,
|
|
url,
|
|
method,
|
|
payload,
|
|
headers,
|
|
route
|
|
);
|
|
}
|
|
|
|
return response;
|
|
} finally {
|
|
if (target.created && target.tab.id) {
|
|
await chrome.tabs.remove(target.tab.id).catch(() => {});
|
|
}
|
|
|
|
await restoreActiveTab(activeTab);
|
|
}
|
|
}
|
|
|
|
function getComcipRoute(requestUrl) {
|
|
const normalizedUrl = String(requestUrl || "");
|
|
|
|
if (normalizedUrl.includes("/ic/builder/rt/oalset_timeentrymobile/live")) {
|
|
return {
|
|
appUrl: COMCIP_TIME_ENTRY_APP_URL,
|
|
clientIdProbeUrl: COMCIP_TIME_ENTRY_CLIENT_ID_PROBE_URL,
|
|
tabPattern: COMCIP_TIME_ENTRY_TAB_URL_PATTERN,
|
|
useSenderFrame: true,
|
|
appVersion: COMCIP_TIME_ENTRY_APP_VERSION,
|
|
};
|
|
}
|
|
|
|
return {
|
|
appUrl: COMCIP_APP_URL,
|
|
clientIdProbeUrl: COMCIP_CLIENT_ID_PROBE_URL,
|
|
tabPattern: COMCIP_TAB_URL_PATTERN,
|
|
};
|
|
}
|
|
|
|
async function getComcipTab(route, activeTab) {
|
|
const tabs = await chrome.tabs.query({ url: route.tabPattern });
|
|
const existing = tabs.find((tab) => {
|
|
const tabUrl = String(tab.url || "");
|
|
|
|
return tab.id && !tab.discarded && !tabUrl.includes("/services/");
|
|
});
|
|
|
|
if (existing) {
|
|
return {
|
|
tab: existing,
|
|
created: false,
|
|
};
|
|
}
|
|
|
|
if (route.requireExistingTab) {
|
|
throw new Error(
|
|
"A sessao COMCIP Time Entry nao esta pronta. Abra/autentique o Time Entry uma vez e tente novamente."
|
|
);
|
|
}
|
|
|
|
const created = await chrome.tabs.create({
|
|
url: route.appUrl,
|
|
active: false,
|
|
windowId: activeTab?.windowId,
|
|
});
|
|
await restoreActiveTab(activeTab);
|
|
|
|
return {
|
|
tab: created,
|
|
created: true,
|
|
};
|
|
}
|
|
|
|
async function captureActiveTab() {
|
|
const [activeTab] = await chrome.tabs.query({
|
|
active: true,
|
|
currentWindow: true,
|
|
});
|
|
|
|
if (!activeTab?.id) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
id: activeTab.id,
|
|
windowId: activeTab.windowId,
|
|
};
|
|
}
|
|
|
|
async function restoreActiveTab(activeTab) {
|
|
if (!activeTab?.id) {
|
|
return;
|
|
}
|
|
|
|
await chrome.windows.update(activeTab.windowId, { focused: true }).catch(() => {});
|
|
await chrome.tabs.update(activeTab.id, { active: true }).catch(() => {});
|
|
}
|
|
|
|
async function waitForComcipTabReady(tabId) {
|
|
const startedAt = Date.now();
|
|
|
|
while (Date.now() - startedAt < 45000) {
|
|
const tab = await chrome.tabs.get(tabId);
|
|
const url = String(tab.url || "");
|
|
|
|
if (tab.status === "complete" && url.startsWith(COMCIP_ORIGIN)) {
|
|
await delay(1000);
|
|
return;
|
|
}
|
|
|
|
await delay(500);
|
|
}
|
|
|
|
throw new Error("Timed out while loading the COMCIP origin page.");
|
|
}
|
|
|
|
async function waitForComcipSessionReady(tabId, headers = {}, route) {
|
|
const startedAt = Date.now();
|
|
let lastStatus = "";
|
|
|
|
while (Date.now() - startedAt < 60000) {
|
|
const probe = await executeComcipProbe(tabId, headers, route).catch((error) => ({
|
|
ok: false,
|
|
status: 0,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
}));
|
|
|
|
if (probe?.ok && probe?.appBuilderClientId) {
|
|
return probe;
|
|
}
|
|
|
|
lastStatus = probe?.status
|
|
? `${probe.status} ${probe.statusText || ""}`.trim()
|
|
: probe?.error || "not ready";
|
|
await delay(1000);
|
|
}
|
|
|
|
throw new Error(`Timed out while waiting for COMCIP authenticated session (${lastStatus}).`);
|
|
}
|
|
|
|
async function waitForComcipFrameSessionReady(tabId, headers = {}, route) {
|
|
const startedAt = Date.now();
|
|
let lastStatus = "";
|
|
|
|
while (Date.now() - startedAt < 60000) {
|
|
const probe = await executeComcipFrameProbe(tabId, headers, route).catch((error) => ({
|
|
ok: false,
|
|
status: 0,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
}));
|
|
|
|
if (probe?.ok && probe?.matched) {
|
|
return probe;
|
|
}
|
|
|
|
lastStatus = probe?.status
|
|
? `${probe.status} ${probe.statusText || ""}`.trim()
|
|
: probe?.error || "not ready";
|
|
await delay(1000);
|
|
}
|
|
|
|
throw new Error(
|
|
`Timed out while waiting for COMCIP Time Entry frame session (${lastStatus}).`
|
|
);
|
|
}
|
|
|
|
async function executeComcipFrameProbe(tabId, headers = {}, route) {
|
|
const injectionResults = await chrome.scripting.executeScript({
|
|
target: { tabId, allFrames: true },
|
|
world: "MAIN",
|
|
args: [
|
|
route?.clientIdProbeUrl || COMCIP_CLIENT_ID_PROBE_URL,
|
|
headers || {},
|
|
route?.appVersion || "",
|
|
],
|
|
func: async (probeUrl, sourceHeaders, routeAppVersion) => {
|
|
function isTimeEntryFrame() {
|
|
const href = String(window.location.href || "");
|
|
const frameName = String(window.name || "");
|
|
const isComcipOrigin = href.startsWith(
|
|
"https://comcipapic-oalprod.integration.ocp.oraclecloud.com/"
|
|
);
|
|
|
|
return (
|
|
(frameName === "arch-panel-extension-comcip-timeentry-frame" &&
|
|
isComcipOrigin) ||
|
|
href.includes("oalset_timeentrymobile") ||
|
|
href.includes("/webApps/timeentry")
|
|
);
|
|
}
|
|
|
|
if (!isTimeEntryFrame()) {
|
|
return {
|
|
matched: false,
|
|
href: window.location.href,
|
|
frameName: window.name || "",
|
|
};
|
|
}
|
|
|
|
const response = await fetch(probeUrl, {
|
|
method: "GET",
|
|
credentials: "include",
|
|
cache: "no-store",
|
|
headers: {
|
|
accept: "*/*",
|
|
authorization: "Session",
|
|
"accept-language":
|
|
sourceHeaders["accept-language"] || navigator.language || "pt-BR",
|
|
"vb-proxy-header-rest-framework-version": "3",
|
|
"x-vb-application-version":
|
|
routeAppVersion || sourceHeaders["x-vb-application-version"] || "",
|
|
},
|
|
});
|
|
|
|
return {
|
|
matched: true,
|
|
ok: response.ok,
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
appBuilderClientId: response.headers.get("x-appbuilder-client-id") || "",
|
|
href: window.location.href,
|
|
frameName: window.name || "",
|
|
};
|
|
},
|
|
});
|
|
|
|
const matchedResult = (injectionResults || [])
|
|
.map((item) => item.result)
|
|
.find((result) => result?.matched);
|
|
|
|
if (!matchedResult) {
|
|
const frameHints = (injectionResults || [])
|
|
.map((item) => item.result)
|
|
.filter(Boolean)
|
|
.map((result) => result.href || result.frameName || "unknown")
|
|
.slice(0, 5)
|
|
.join(" | ");
|
|
throw new Error(
|
|
`COMCIP Time Entry frame was not found in the dashboard tab.${
|
|
frameHints ? ` Frames: ${frameHints}` : ""
|
|
}`
|
|
);
|
|
}
|
|
|
|
return matchedResult;
|
|
}
|
|
|
|
async function executeComcipProbe(tabId, headers = {}, route) {
|
|
const [injectionResult] = await chrome.scripting.executeScript({
|
|
target: { tabId },
|
|
world: "MAIN",
|
|
args: [route?.clientIdProbeUrl || COMCIP_CLIENT_ID_PROBE_URL, headers || {}],
|
|
func: async (probeUrl, sourceHeaders) => {
|
|
const APP_VERSION = "version_1754044416761";
|
|
const response = await fetch(probeUrl, {
|
|
method: "GET",
|
|
credentials: "include",
|
|
cache: "no-store",
|
|
headers: {
|
|
accept: "*/*",
|
|
authorization: "Session",
|
|
"accept-language":
|
|
sourceHeaders["accept-language"] || navigator.language || "pt-BR",
|
|
"x-vb-application-version":
|
|
sourceHeaders["x-vb-application-version"] || APP_VERSION,
|
|
},
|
|
});
|
|
|
|
return {
|
|
ok: response.ok,
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
appBuilderClientId: response.headers.get("x-appbuilder-client-id") || "",
|
|
href: window.location.href,
|
|
};
|
|
},
|
|
});
|
|
|
|
if (!injectionResult) {
|
|
throw new Error("COMCIP probe did not return a result.");
|
|
}
|
|
|
|
return injectionResult.result;
|
|
}
|
|
|
|
async function executeComcipFetch(tabId, requestUrl, method, payload, headers, route) {
|
|
const [injectionResult] = await chrome.scripting.executeScript({
|
|
target: { tabId },
|
|
world: "MAIN",
|
|
args: [
|
|
requestUrl,
|
|
method || "POST",
|
|
payload || null,
|
|
headers || {},
|
|
route?.clientIdProbeUrl || COMCIP_CLIENT_ID_PROBE_URL,
|
|
route?.appVersion || "",
|
|
],
|
|
func: async (
|
|
requestUrl,
|
|
requestMethod,
|
|
requestPayload,
|
|
sourceHeaders,
|
|
clientIdProbeUrl,
|
|
routeAppVersion
|
|
) => {
|
|
const APP_VERSION = "version_1754044416761";
|
|
|
|
function parseResponseText(text) {
|
|
if (!text) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
return JSON.parse(text);
|
|
} catch {
|
|
return text;
|
|
}
|
|
}
|
|
|
|
function pickResponseHeaders(headers) {
|
|
const safeHeaders = {};
|
|
const exposedHeaders = [
|
|
"content-type",
|
|
"x-appbuilder-client-id",
|
|
"x-invalid-appbuilder-client-id",
|
|
"x-appbuilder-repeat-request",
|
|
"vb-proxy-status-actual",
|
|
"vb-proxy-version",
|
|
];
|
|
|
|
for (const key of exposedHeaders) {
|
|
const value = headers.get(key);
|
|
|
|
if (value) {
|
|
safeHeaders[key] = value;
|
|
}
|
|
}
|
|
|
|
return safeHeaders;
|
|
}
|
|
|
|
async function resolveAppBuilderClientId() {
|
|
try {
|
|
const response = await fetch(clientIdProbeUrl, {
|
|
method: "GET",
|
|
credentials: "include",
|
|
cache: "no-store",
|
|
headers: {
|
|
accept: "*/*",
|
|
authorization: "Session",
|
|
"accept-language":
|
|
sourceHeaders["accept-language"] || navigator.language || "pt-BR",
|
|
"x-vb-application-version":
|
|
sourceHeaders["x-vb-application-version"] || APP_VERSION,
|
|
},
|
|
});
|
|
|
|
return response.headers.get("x-appbuilder-client-id") || "";
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
const appBuilderClientId =
|
|
sourceHeaders["x-appbuilder-client-id"] || (await resolveAppBuilderClientId());
|
|
const requestHeaders = {
|
|
accept: "*/*",
|
|
authorization: "Session",
|
|
"vb-proxy-header-preference": "transient",
|
|
"x-vb-application-version":
|
|
sourceHeaders["x-vb-application-version"] || APP_VERSION,
|
|
"accept-language":
|
|
sourceHeaders["accept-language"] || navigator.language || "pt-BR",
|
|
};
|
|
|
|
if (appBuilderClientId) {
|
|
requestHeaders["x-appbuilder-client-id"] = appBuilderClientId;
|
|
}
|
|
|
|
const normalizedMethod = String(requestMethod || "POST").toUpperCase();
|
|
const fetchOptions = {
|
|
method: normalizedMethod,
|
|
credentials: "include",
|
|
cache: "no-store",
|
|
headers: requestHeaders,
|
|
};
|
|
|
|
if (
|
|
normalizedMethod !== "GET" &&
|
|
normalizedMethod !== "HEAD" &&
|
|
requestPayload !== undefined &&
|
|
requestPayload !== null
|
|
) {
|
|
requestHeaders["content-type"] = "application/json";
|
|
fetchOptions.body = JSON.stringify(requestPayload);
|
|
}
|
|
|
|
const response = await fetch(requestUrl, {
|
|
...fetchOptions,
|
|
});
|
|
const text = await response.text();
|
|
|
|
return {
|
|
ok: response.ok,
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
headers: pickResponseHeaders(response.headers),
|
|
payload: parseResponseText(text),
|
|
debug: {
|
|
href: window.location.href,
|
|
origin: window.location.origin,
|
|
headerKeys: Object.keys(requestHeaders),
|
|
},
|
|
};
|
|
},
|
|
});
|
|
|
|
if (!injectionResult) {
|
|
throw new Error("COMCIP page script did not return a result.");
|
|
}
|
|
|
|
return injectionResult.result;
|
|
}
|
|
|
|
async function executeComcipFetchFromFrame(
|
|
tabId,
|
|
requestUrl,
|
|
method,
|
|
payload,
|
|
headers,
|
|
route
|
|
) {
|
|
const injectionResults = await chrome.scripting.executeScript({
|
|
target: { tabId, allFrames: true },
|
|
world: "MAIN",
|
|
args: [
|
|
requestUrl,
|
|
method || "POST",
|
|
payload || null,
|
|
headers || {},
|
|
route?.clientIdProbeUrl || COMCIP_CLIENT_ID_PROBE_URL,
|
|
route?.appVersion || "",
|
|
],
|
|
func: async (
|
|
requestUrl,
|
|
requestMethod,
|
|
requestPayload,
|
|
sourceHeaders,
|
|
clientIdProbeUrl,
|
|
routeAppVersion
|
|
) => {
|
|
function isTimeEntryFrame() {
|
|
const href = String(window.location.href || "");
|
|
const frameName = String(window.name || "");
|
|
const isComcipOrigin = href.startsWith(
|
|
"https://comcipapic-oalprod.integration.ocp.oraclecloud.com/"
|
|
);
|
|
|
|
return (
|
|
(frameName === "arch-panel-extension-comcip-timeentry-frame" &&
|
|
isComcipOrigin) ||
|
|
href.includes("oalset_timeentrymobile") ||
|
|
href.includes("/webApps/timeentry")
|
|
);
|
|
}
|
|
|
|
if (!isTimeEntryFrame()) {
|
|
return {
|
|
matched: false,
|
|
href: window.location.href,
|
|
frameName: window.name || "",
|
|
};
|
|
}
|
|
|
|
function parseResponseText(text) {
|
|
if (!text) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
return JSON.parse(text);
|
|
} catch {
|
|
return text;
|
|
}
|
|
}
|
|
|
|
function pickResponseHeaders(responseHeaders) {
|
|
const safeHeaders = {};
|
|
const exposedHeaders = [
|
|
"content-type",
|
|
"x-appbuilder-client-id",
|
|
"x-invalid-appbuilder-client-id",
|
|
"x-appbuilder-repeat-request",
|
|
"vb-proxy-status-actual",
|
|
"vb-proxy-version",
|
|
];
|
|
|
|
for (const key of exposedHeaders) {
|
|
const value = responseHeaders.get(key);
|
|
|
|
if (value) {
|
|
safeHeaders[key] = value;
|
|
}
|
|
}
|
|
|
|
return safeHeaders;
|
|
}
|
|
|
|
async function resolveAppBuilderClientId() {
|
|
try {
|
|
const response = await fetch(clientIdProbeUrl, {
|
|
method: "GET",
|
|
credentials: "include",
|
|
cache: "no-store",
|
|
headers: {
|
|
accept: "*/*",
|
|
authorization: "Session",
|
|
"accept-language":
|
|
sourceHeaders["accept-language"] || navigator.language || "pt-BR",
|
|
"vb-proxy-header-rest-framework-version": "3",
|
|
"x-vb-application-version":
|
|
routeAppVersion || sourceHeaders["x-vb-application-version"] || "",
|
|
},
|
|
});
|
|
|
|
return response.headers.get("x-appbuilder-client-id") || "";
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
const appBuilderClientId =
|
|
sourceHeaders["x-appbuilder-client-id"] || (await resolveAppBuilderClientId());
|
|
const requestHeaders = {
|
|
accept: "*/*",
|
|
authorization: "Session",
|
|
"vb-proxy-header-preference": "transient",
|
|
"vb-proxy-header-rest-framework-version": "3",
|
|
"x-vb-application-version":
|
|
routeAppVersion || sourceHeaders["x-vb-application-version"] || "",
|
|
"accept-language":
|
|
sourceHeaders["accept-language"] || navigator.language || "pt-BR",
|
|
};
|
|
|
|
if (appBuilderClientId) {
|
|
requestHeaders["x-appbuilder-client-id"] = appBuilderClientId;
|
|
}
|
|
|
|
const normalizedMethod = String(requestMethod || "POST").toUpperCase();
|
|
const fetchOptions = {
|
|
method: normalizedMethod,
|
|
credentials: "include",
|
|
cache: "no-store",
|
|
headers: requestHeaders,
|
|
};
|
|
|
|
if (
|
|
normalizedMethod !== "GET" &&
|
|
normalizedMethod !== "HEAD" &&
|
|
requestPayload !== undefined &&
|
|
requestPayload !== null
|
|
) {
|
|
requestHeaders["content-type"] = "application/json";
|
|
fetchOptions.body = JSON.stringify(requestPayload);
|
|
}
|
|
|
|
const response = await fetch(requestUrl, fetchOptions);
|
|
const text = await response.text();
|
|
|
|
return {
|
|
matched: true,
|
|
ok: response.ok,
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
headers: pickResponseHeaders(response.headers),
|
|
payload: parseResponseText(text),
|
|
debug: {
|
|
href: window.location.href,
|
|
origin: window.location.origin,
|
|
frameName: window.name || "",
|
|
headerKeys: Object.keys(requestHeaders),
|
|
frameFetch: true,
|
|
},
|
|
};
|
|
},
|
|
});
|
|
|
|
const matchedResult = (injectionResults || [])
|
|
.map((item) => item.result)
|
|
.find((result) => result?.matched);
|
|
|
|
if (!matchedResult) {
|
|
const frameHints = (injectionResults || [])
|
|
.map((item) => item.result)
|
|
.filter(Boolean)
|
|
.map((result) => result.href || result.frameName || "unknown")
|
|
.slice(0, 5)
|
|
.join(" | ");
|
|
throw new Error(
|
|
`COMCIP Time Entry frame did not return a result.${
|
|
frameHints ? ` Frames: ${frameHints}` : ""
|
|
}`
|
|
);
|
|
}
|
|
|
|
return matchedResult;
|
|
}
|
|
|
|
async function executeComcipDirectFetch(
|
|
requestUrl,
|
|
method = "GET",
|
|
payload = null,
|
|
headers = {},
|
|
route
|
|
) {
|
|
const APP_VERSION = "version_1754044416761";
|
|
|
|
function parseResponseText(text) {
|
|
if (!text) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
return JSON.parse(text);
|
|
} catch {
|
|
return text;
|
|
}
|
|
}
|
|
|
|
function pickResponseHeaders(responseHeaders) {
|
|
const safeHeaders = {};
|
|
const exposedHeaders = [
|
|
"content-type",
|
|
"x-appbuilder-client-id",
|
|
"x-invalid-appbuilder-client-id",
|
|
"x-appbuilder-repeat-request",
|
|
"vb-proxy-status-actual",
|
|
"vb-proxy-version",
|
|
];
|
|
|
|
for (const key of exposedHeaders) {
|
|
const value = responseHeaders.get(key);
|
|
|
|
if (value) {
|
|
safeHeaders[key] = value;
|
|
}
|
|
}
|
|
|
|
return safeHeaders;
|
|
}
|
|
|
|
async function resolveAppBuilderClientId() {
|
|
try {
|
|
const response = await fetch(route?.clientIdProbeUrl || COMCIP_CLIENT_ID_PROBE_URL, {
|
|
method: "GET",
|
|
credentials: "include",
|
|
cache: "no-store",
|
|
headers: {
|
|
accept: "*/*",
|
|
authorization: "Session",
|
|
"accept-language": headers["accept-language"] || "pt-BR",
|
|
},
|
|
});
|
|
|
|
return response.headers.get("x-appbuilder-client-id") || "";
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
const appBuilderClientId =
|
|
headers["x-appbuilder-client-id"] || (await resolveAppBuilderClientId());
|
|
const requestHeaders = {
|
|
accept: "*/*",
|
|
authorization: "Session",
|
|
"vb-proxy-header-preference": "transient",
|
|
"accept-language": headers["accept-language"] || "pt-BR",
|
|
};
|
|
|
|
if (!route?.omitAppVersion && headers["x-vb-application-version"]) {
|
|
requestHeaders["x-vb-application-version"] = headers["x-vb-application-version"];
|
|
}
|
|
|
|
if (appBuilderClientId) {
|
|
requestHeaders["x-appbuilder-client-id"] = appBuilderClientId;
|
|
}
|
|
|
|
const normalizedMethod = String(method || "GET").toUpperCase();
|
|
const fetchOptions = {
|
|
method: normalizedMethod,
|
|
credentials: "include",
|
|
cache: "no-store",
|
|
headers: requestHeaders,
|
|
};
|
|
|
|
if (
|
|
normalizedMethod !== "GET" &&
|
|
normalizedMethod !== "HEAD" &&
|
|
payload !== undefined &&
|
|
payload !== null
|
|
) {
|
|
requestHeaders["content-type"] = "application/json";
|
|
fetchOptions.body = JSON.stringify(payload);
|
|
}
|
|
|
|
const response = await fetch(requestUrl, fetchOptions);
|
|
const text = await response.text();
|
|
|
|
return {
|
|
ok: response.ok,
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
headers: pickResponseHeaders(response.headers),
|
|
payload: parseResponseText(text),
|
|
debug: {
|
|
directFetch: true,
|
|
origin: chrome.runtime.getURL(""),
|
|
headerKeys: Object.keys(requestHeaders),
|
|
},
|
|
};
|
|
}
|
|
|
|
function isAuthorizationFailure(response) {
|
|
if (!response) {
|
|
return false;
|
|
}
|
|
|
|
const payloadText =
|
|
typeof response.payload === "string"
|
|
? response.payload
|
|
: JSON.stringify(response.payload || "");
|
|
|
|
return (
|
|
response.status === 401 ||
|
|
response.status === 500 && /vb:\/\/trap\/model\/no_auth/i.test(payloadText) ||
|
|
/No user authentication for the service/i.test(payloadText) ||
|
|
/missingAnonymous/i.test(payloadText) ||
|
|
/401 Authorization Required/i.test(payloadText) ||
|
|
/Authorization Required/i.test(payloadText)
|
|
);
|
|
}
|
|
|
|
function isOutdatedApplicationFailure(response) {
|
|
if (!response) {
|
|
return false;
|
|
}
|
|
|
|
const payloadText =
|
|
typeof response.payload === "string"
|
|
? response.payload
|
|
: JSON.stringify(response.payload || "");
|
|
|
|
return /out-dated version of the application/i.test(payloadText);
|
|
}
|
|
|
|
function delay(milliseconds) {
|
|
return new Promise((resolve) => {
|
|
setTimeout(resolve, milliseconds);
|
|
});
|
|
}
|