434 lines
18 KiB
JavaScript
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);
|