Publish Auto Mirror Switcher 1.4.3 with installation and privacy documentation
This commit is contained in:
commit
5052f542de
33 files changed
+4912
No files matched your search
+433
@@ -0,0 +1,433 @@
|
||||
(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);
|
||||
Reference in new issue
Block a user