(function (root) { 'use strict'; var S = root.AutoMirrorShared; var D = root.AutoMirrorDiagnostics; var C = root.AutoMirrorCache; var P = root.AutoMirrorProbe; var R = root.AutoMirrorRuntimeState; D.initializeDebugLogs(); function getConfig(callback) { chrome.storage.local.get({ enabled: true, rules: S.getDefaultRules() }, function (items) { callback({ enabled: items.enabled !== false, rules: Array.isArray(items.rules) ? items.rules : [] }); }); } function initializeDefaults() { chrome.storage.local.get(['rules'], function (items) { if (!Array.isArray(items.rules)) { chrome.storage.local.set({ enabled: true, rules: S.getDefaultRules() }); } }); } function reportBadRuleRegex(rule, error) { D.warn('Bad hostRegex in rule:', rule && (rule.name || rule.id), error && error.message || String(error)); } function findRuleForHost(rules, hostname) { return S.findRuleForHost(rules, hostname, function (error, rule) { reportBadRuleRegex(rule, error); }); } function ensureEventIsCurrent(tabId, rawUrl, reason, callback) { try { R.getLastMainFrameUrl(tabId, function (lastMainFrameUrl) { chrome.tabs.get(tabId, function (tab) { if (chrome.runtime.lastError || !tab || !tab.url) { D.appendDebugLog('warn', 'trigger.staleIgnored', { tabId: tabId, eventUrl: rawUrl, lastMainFrameUrl: lastMainFrameUrl, reason: reason, error: chrome.runtime.lastError && chrome.runtime.lastError.message || '' }); callback(Boolean(lastMainFrameUrl && S.urlsReferToSameNavigation(lastMainFrameUrl, rawUrl))); return; } var tabMatches = S.urlsReferToSameNavigation(tab.url, rawUrl); var lastMatches = Boolean(lastMainFrameUrl && S.urlsReferToSameNavigation(lastMainFrameUrl, rawUrl)); if (lastMainFrameUrl) { if (!lastMatches) { D.appendDebugLog('warn', 'trigger.staleIgnored', { tabId: tabId, eventUrl: rawUrl, currentTabUrl: tab.url, lastMainFrameUrl: lastMainFrameUrl, reason: reason }); callback(false); return; } if (!tabMatches) { D.appendDebugLog('debug', 'trigger.currentByLastNavigationUrl', { tabId: tabId, eventUrl: rawUrl, currentTabUrl: tab.url, lastMainFrameUrl: lastMainFrameUrl, reason: reason }); } callback(true); return; } if (!tabMatches) { D.appendDebugLog('warn', 'trigger.staleIgnored', { tabId: tabId, eventUrl: rawUrl, currentTabUrl: tab.url, lastMainFrameUrl: lastMainFrameUrl, reason: reason }); callback(false); return; } callback(true); }); }); } catch (e) { D.appendDebugLog('warn', 'trigger.currentTabCheckFailed', { tabId: tabId, eventUrl: rawUrl, reason: reason, error: e && e.message || String(e) }); callback(false); } } function getTabState(tabId, rule, sourceUrlObj, callback) { R.getTabState(tabId, rule, sourceUrlObj, callback); } function logBuiltCandidates(rule, currentHost, cachedHost, candidates) { D.appendDebugLog('debug', 'candidates.built', { rule: rule.name || rule.id || 'unknown', currentHost: currentHost, cachedHost: cachedHost || '', count: candidates.length, first: candidates.slice(0, 12), last: candidates.slice(Math.max(0, candidates.length - 5)) }); } function chooseNextCandidate(rule, sourceUrlObj, state, callback) { C.getCachedHost(rule, function (cachedHost) { var currentHost = S.normalizeHost(sourceUrlObj.hostname); var candidates = S.buildCandidates(rule, currentHost, cachedHost); logBuiltCandidates(rule, currentHost, cachedHost, candidates); D.appendDebugLog('debug', 'step.choose.start', { rule: rule.name || rule.id || 'unknown', currentHost: currentHost, path: S.getPath(sourceUrlObj), cachedHost: cachedHost || '', triedHosts: Object.keys(state.triedHosts || {}), candidatesCount: candidates.length, candidatesFirst: candidates.slice(0, 15), candidatesLast: candidates.slice(Math.max(0, candidates.length - 10)) }); for (var i = 0; i < candidates.length; i += 1) { if (!state.triedHosts[candidates[i]]) { D.appendDebugLog('info', 'step.choose.selected', { rule: rule.name || rule.id || 'unknown', selectedHost: candidates[i], selectedIndex: i, currentHost: currentHost }); callback(candidates[i]); return; } } D.appendDebugLog('warn', 'step.choose.none', { rule: rule.name || rule.id || 'unknown', currentHost: currentHost, triedHosts: Object.keys(state.triedHosts || {}), candidatesCount: candidates.length }); callback(null); }); } function showStoppedPage(tabId, rule, sourceUrlObj, state, reason) { var stopId = 'stop:' + tabId + ':' + Date.now(); var tried = Object.keys(state && state.triedHosts || {}); var data = {}; data[stopId] = { time: new Date().toISOString(), rule: rule && (rule.name || rule.id) || 'unknown', sourceUrl: sourceUrlObj && sourceUrlObj.href || '', path: sourceUrlObj ? S.getPath(sourceUrlObj) : '', reason: reason || 'stopped', triedHosts: tried, redirects: state && state.redirects || 0, range: S.getNumberRange(rule || {}) }; D.appendDebugLog('warn', 'stopPage.open', data[stopId]); chrome.storage.local.set(data, function () { chrome.tabs.update(tabId, { url: chrome.runtime.getURL('stopped.html') + '#' + encodeURIComponent(stopId) }); }); } function redirectTab(tabId, rule, host, sourceUrlObj, state, reason) { var target = S.buildTargetUrl(rule, host, sourceUrlObj); state.redirects += 1; state.triedHosts[host] = true; state.lastRedirectFrom = sourceUrlObj.href; state.lastRedirectTo = target; state.lastRedirectAt = Date.now(); D.appendDebugLog('info', 'redirect', { tabId: tabId, rule: rule.name || rule.id || 'unknown', from: sourceUrlObj.href, fromHost: S.normalizeHost(sourceUrlObj.hostname), to: target, toHost: host, reason: reason, redirects: state.redirects, triedHosts: Object.keys(state.triedHosts || {}) }); D.log('Redirecting tab', tabId, 'to', target, 'reason:', reason); R.setLastMainFrameUrl(tabId, target); R.saveTabState(tabId, state); chrome.tabs.update(tabId, { url: target }); } function maybeRedirectToCachedHost(tabId, rawUrl) { if (typeof tabId !== 'number' || tabId < 0) return; var sourceUrlObj = S.safeUrl(rawUrl); if (!sourceUrlObj || !/^https?:$/.test(sourceUrlObj.protocol)) return; getConfig(function (config) { if (!config.enabled) return; var currentHost = S.normalizeHost(sourceUrlObj.hostname); var rule = findRuleForHost(config.rules, currentHost); if (!rule) return; D.appendDebugLog('debug', 'beforeNavigate.ruleMatched', { tabId: tabId, url: rawUrl, host: currentHost, rule: rule.name || rule.id || 'unknown' }); if (!S.preferCachedHostEnabled(rule)) { D.appendDebugLog('debug', 'cachedRedirect.disabled', { tabId: tabId, host: currentHost, rule: rule.name || rule.id || 'unknown' }); return; } C.getCachedHost(rule, function (cachedHost) { cachedHost = S.normalizeHost(cachedHost); if (!cachedHost || cachedHost === currentHost) { D.appendDebugLog('debug', 'cachedRedirect.skipped', { tabId: tabId, host: currentHost, cachedHost: cachedHost || '', reason: !cachedHost ? 'no-cache' : 'same-host' }); return; } if (!S.matchesRule(rule, cachedHost)) { D.appendDebugLog('warn', 'cachedRedirect.invalidCachedHost', { tabId: tabId, host: currentHost, cachedHost: cachedHost, rule: rule.name || rule.id || 'unknown' }); return; } getTabState(tabId, rule, sourceUrlObj, function (state) { if (state.redirects >= S.getEffectiveMaxRedirects(rule)) return; if (state.triedHosts[currentHost]) return; state.triedHosts[currentHost] = true; redirectTab(tabId, rule, cachedHost, sourceUrlObj, state, 'cached-host'); }); }); }); } function triggerRedirect(tabId, rawUrl, reason) { if (typeof tabId !== 'number' || tabId < 0) return; var sourceUrlObj = S.safeUrl(rawUrl); if (!sourceUrlObj || !/^https?:$/.test(sourceUrlObj.protocol)) return; ensureEventIsCurrent(tabId, rawUrl, reason, function (isCurrent) { if (!isCurrent) return; getConfig(function (config) { if (!config.enabled) return; var currentHost = S.normalizeHost(sourceUrlObj.hostname); var rule = findRuleForHost(config.rules, currentHost); if (!rule) { D.appendDebugLog('debug', 'trigger.noRule', { tabId: tabId, url: rawUrl, host: currentHost, reason: reason }); return; } D.appendDebugLog('info', 'trigger', { tabId: tabId, url: rawUrl, host: currentHost, reason: reason, rule: rule.name || rule.id || 'unknown', mode: rule.mode || 'step', strategy: rule.strategy || 'number', range: S.getNumberRange(rule), includeEmpty: rule.includeEmpty === true, template: rule.template || '' }); C.clearCachedHostIfMatches(rule, currentHost, reason); getTabState(tabId, rule, sourceUrlObj, function (state) { var maxRedirects = S.getEffectiveMaxRedirects(rule); var now = Date.now(); if (state.lastTriggerUrl === sourceUrlObj.href && now - Number(state.lastTriggerAt || 0) < 2500) { D.appendDebugLog('warn', 'trigger.duplicateIgnored', { tabId: tabId, url: sourceUrlObj.href, reason: reason, previousAt: state.lastTriggerAt || 0 }); return; } state.lastTriggerUrl = sourceUrlObj.href; state.lastTriggerAt = now; state.triedHosts[currentHost] = true; R.saveTabState(tabId, state); if (state.redirects >= maxRedirects) { D.warn('Max redirects reached for tab', tabId, 'rule:', rule.name || rule.id, 'effective limit:', maxRedirects); showStoppedPage(tabId, rule, sourceUrlObj, state, 'max-redirects:' + reason); return; } if (rule.mode === 'probe') { P.findWorkingByProbe(rule, sourceUrlObj, state.triedHosts, function (host) { if (!host) { D.warn('No working mirror found by probe for rule:', rule.name || rule.id); showStoppedPage(tabId, rule, sourceUrlObj, state, 'probe-no-working-host:' + reason); return; } C.setCachedHost(rule, host); redirectTab(tabId, rule, host, sourceUrlObj, state, reason + '/probe'); }); } else { chooseNextCandidate(rule, sourceUrlObj, state, function (host) { if (!host) { D.warn('No next candidate found for rule:', rule.name || rule.id); showStoppedPage(tabId, rule, sourceUrlObj, state, 'no-next-candidate:' + reason); return; } redirectTab(tabId, rule, host, sourceUrlObj, state, reason + '/step'); }); } }); }); }); } chrome.runtime.onInstalled.addListener(initializeDefaults); initializeDefaults(); chrome.action.onClicked.addListener(function () { if (chrome.runtime.openOptionsPage) { chrome.runtime.openOptionsPage(); } else { chrome.tabs.create({ url: chrome.runtime.getURL('options.html') }); } }); chrome.webNavigation.onBeforeNavigate.addListener(function (details) { if (details.frameId !== 0) return; R.setLastMainFrameUrl(details.tabId, details.url); maybeRedirectToCachedHost(details.tabId, details.url); }); chrome.webNavigation.onCommitted.addListener(function (details) { if (details.frameId !== 0) return; R.setLastMainFrameUrl(details.tabId, details.url); }); chrome.tabs.onRemoved.addListener(function (tabId) { R.clearTab(tabId); }); chrome.webNavigation.onErrorOccurred.addListener(function (details) { if (details.frameId !== 0) return; triggerRedirect(details.tabId, details.url, 'navigation-error:' + (details.error || 'unknown')); }); chrome.webRequest.onCompleted.addListener(function (details) { if (!details || details.type !== 'main_frame') return; var status = Number(details.statusCode || 0); if (status < 500 && status !== 403 && status !== 451) return; triggerRedirect(details.tabId, details.url, 'http-status:' + status); }, { urls: ['http://*/*', 'https://*/*'] }); chrome.runtime.onMessage.addListener(function (message, sender) { if (!message) return; if (message.type === 'DEBUG_LOG') { D.appendDebugLog(message.level || 'debug', message.event || 'content.debug', Object.assign({ tabId: sender && sender.tab ? sender.tab.id : null, senderUrl: sender && sender.url ? sender.url : '' }, message.data || {})); return; } if (!sender || !sender.tab) return; if (message.type === 'BLOCKED_PAGE_DETECTED') { D.appendDebugLog('warn', 'content.blockedPageDetected', { tabId: sender.tab.id, url: message.url, marker: message.marker || '', rule: message.rule || '', title: message.title || '' }); triggerRedirect(sender.tab.id, message.url, 'content-blocked-page:' + (message.marker || 'marker')); return; } if (message.type === 'PAGE_OK') { D.appendDebugLog('info', 'content.pageOk', { tabId: sender.tab.id, url: message.url, rule: message.rule || '', title: message.title || '' }); var urlObj = S.safeUrl(message.url); if (!urlObj) return; ensureEventIsCurrent(sender.tab.id, message.url, 'content-page-ok', function (isCurrent) { if (!isCurrent) return; getConfig(function (config) { var rule = findRuleForHost(config.rules, urlObj.hostname); if (!rule) return; C.setCachedHost(rule, urlObj.hostname); R.clearTab(sender.tab.id); D.log('Cached working host:', urlObj.hostname, 'for rule:', rule.name || rule.id); }); }); } }); })(typeof globalThis !== 'undefined' ? globalThis : this);