Files

434 lines
18 KiB
JavaScript

(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);