|
|
@ -150,7 +150,15 @@ housekeep itself. |
|
|
|
var mostRecentRootDocURL = ''; |
|
|
|
var mostRecentRootDocURLTimestamp = 0; |
|
|
|
|
|
|
|
var gcPeriod = 10 * 60 * 1000; |
|
|
|
var gcPeriod = 31 * 60 * 1000; // every 31 minutes
|
|
|
|
|
|
|
|
// A pushed entry is removed from the stack unless it is committed with
|
|
|
|
// a set time.
|
|
|
|
var StackEntry = function(url, commit) { |
|
|
|
this.url = url; |
|
|
|
this.committed = commit; |
|
|
|
this.tstamp = Date.now(); |
|
|
|
}; |
|
|
|
|
|
|
|
var TabContext = function(tabId) { |
|
|
|
this.tabId = tabId; |
|
|
@ -161,9 +169,8 @@ housekeep itself. |
|
|
|
this.rootHostname = |
|
|
|
this.rootDomain = ''; |
|
|
|
this.secure = false; |
|
|
|
this.timer = null; |
|
|
|
this.onTabCallback = null; |
|
|
|
this.onTimerCallback = null; |
|
|
|
this.commitTimer = null; |
|
|
|
this.gcTimer = null; |
|
|
|
|
|
|
|
tabContexts[tabId] = this; |
|
|
|
}; |
|
|
@ -172,119 +179,122 @@ housekeep itself. |
|
|
|
if ( vAPI.isBehindTheSceneTabId(this.tabId) ) { |
|
|
|
return; |
|
|
|
} |
|
|
|
if ( this.timer !== null ) { |
|
|
|
clearTimeout(this.timer); |
|
|
|
this.timer = null; |
|
|
|
if ( this.gcTimer !== null ) { |
|
|
|
clearTimeout(this.gcTimer); |
|
|
|
this.gcTimer = null; |
|
|
|
} |
|
|
|
delete tabContexts[this.tabId]; |
|
|
|
}; |
|
|
|
|
|
|
|
TabContext.prototype.onTab = function(tab) { |
|
|
|
if ( tab ) { |
|
|
|
this.timer = vAPI.setTimeout(this.onTimerCallback, gcPeriod); |
|
|
|
this.gcTimer = vAPI.setTimeout(this.onGC.bind(this), gcPeriod); |
|
|
|
} else { |
|
|
|
this.destroy(); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
TabContext.prototype.onTimer = function() { |
|
|
|
this.timer = null; |
|
|
|
TabContext.prototype.onGC = function() { |
|
|
|
this.gcTimer = null; |
|
|
|
if ( vAPI.isBehindTheSceneTabId(this.tabId) ) { |
|
|
|
return; |
|
|
|
} |
|
|
|
vAPI.tabs.get(this.tabId, this.onTab.bind(this)); |
|
|
|
}; |
|
|
|
|
|
|
|
// https://github.com/gorhill/uBlock/issues/248
|
|
|
|
// Stack entries have to be committed to stick. Non-committed stack
|
|
|
|
// entries are removed after a set delay.
|
|
|
|
TabContext.prototype.onCommit = function() { |
|
|
|
if ( vAPI.isBehindTheSceneTabId(this.tabId) ) { |
|
|
|
return; |
|
|
|
} |
|
|
|
vAPI.tabs.get(this.tabId, this.onTabCallback); |
|
|
|
this.commitTimer = null; |
|
|
|
// Remove uncommitted entries at the top of the stack.
|
|
|
|
var i = this.stack.length; |
|
|
|
while ( i-- ) { |
|
|
|
if ( this.stack[i].committed ) { |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
// https://github.com/gorhill/uBlock/issues/300
|
|
|
|
// If no committed entry was found, fall back on the bottom-most one
|
|
|
|
// as being the committed one by default.
|
|
|
|
if ( i === -1 && this.stack.length !== 0 ) { |
|
|
|
this.stack[0].committed = true; |
|
|
|
i = 0; |
|
|
|
} |
|
|
|
i += 1; |
|
|
|
if ( i < this.stack.length ) { |
|
|
|
this.stack.length = i; |
|
|
|
this.update(); |
|
|
|
µm.bindTabToPageStats(this.tabId, 'newURL'); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
// This takes care of orphanized tab contexts. Can't be started for all
|
|
|
|
// contexts, as the behind-the-scene context is permanent -- so we do not
|
|
|
|
// want to slush it.
|
|
|
|
// want to flush it.
|
|
|
|
TabContext.prototype.autodestroy = function() { |
|
|
|
if ( vAPI.isBehindTheSceneTabId(this.tabId) ) { |
|
|
|
return; |
|
|
|
} |
|
|
|
this.onTabCallback = this.onTab.bind(this); |
|
|
|
this.onTimerCallback = this.onTimer.bind(this); |
|
|
|
this.timer = vAPI.setTimeout(this.onTimerCallback, gcPeriod); |
|
|
|
this.gcTimer = vAPI.setTimeout(this.onGC.bind(this), gcPeriod); |
|
|
|
}; |
|
|
|
|
|
|
|
// Update just force all properties to be updated to match the most current
|
|
|
|
// Update just force all properties to be updated to match the most recent
|
|
|
|
// root URL.
|
|
|
|
TabContext.prototype.update = function() { |
|
|
|
if ( this.stack.length === 0 ) { |
|
|
|
this.rawURL = |
|
|
|
this.normalURL = |
|
|
|
this.scheme = |
|
|
|
this.rootHostname = |
|
|
|
this.rootDomain = ''; |
|
|
|
} else { |
|
|
|
this.rawURL = this.stack[this.stack.length - 1]; |
|
|
|
this.normalURL = µm.normalizePageURL(this.tabId, this.rawURL); |
|
|
|
this.scheme = µm.URI.schemeFromURI(this.rawURL); |
|
|
|
this.rootHostname = µm.URI.hostnameFromURI(this.normalURL); |
|
|
|
this.rootDomain = µm.URI.domainFromHostname(this.rootHostname) || this.rootHostname; |
|
|
|
this.rawURL = this.normalURL = this.scheme = |
|
|
|
this.rootHostname = this.rootDomain = ''; |
|
|
|
this.secure = false; |
|
|
|
return; |
|
|
|
} |
|
|
|
this.rawURL = this.stack[this.stack.length - 1].url; |
|
|
|
this.normalURL = µm.normalizePageURL(this.tabId, this.rawURL); |
|
|
|
this.scheme = µm.URI.schemeFromURI(this.rawURL); |
|
|
|
this.rootHostname = µm.URI.hostnameFromURI(this.normalURL); |
|
|
|
this.rootDomain = µm.URI.domainFromHostname(this.rootHostname) || this.rootHostname; |
|
|
|
this.secure = µm.URI.isSecureScheme(this.scheme); |
|
|
|
}; |
|
|
|
|
|
|
|
// Called whenever a candidate root URL is spotted for the tab.
|
|
|
|
TabContext.prototype.push = function(url) { |
|
|
|
TabContext.prototype.push = function(url, context) { |
|
|
|
if ( vAPI.isBehindTheSceneTabId(this.tabId) ) { |
|
|
|
return; |
|
|
|
} |
|
|
|
var committed = context !== undefined; |
|
|
|
var count = this.stack.length; |
|
|
|
if ( count !== 0 && this.stack[count - 1] === url ) { |
|
|
|
return; |
|
|
|
} |
|
|
|
this.stack.push(url); |
|
|
|
this.update(); |
|
|
|
}; |
|
|
|
|
|
|
|
// Called when a former push is a false positive:
|
|
|
|
// https://github.com/chrisaljoudi/uBlock/issues/516
|
|
|
|
TabContext.prototype.unpush = function(url) { |
|
|
|
if ( vAPI.isBehindTheSceneTabId(this.tabId) ) { |
|
|
|
return; |
|
|
|
} |
|
|
|
// We are not going to unpush if there is no other candidate, the
|
|
|
|
// point of unpush is to make space for a better candidate.
|
|
|
|
if ( this.stack.length === 1 ) { |
|
|
|
return; |
|
|
|
} |
|
|
|
var pos = this.stack.indexOf(url); |
|
|
|
if ( pos === -1 ) { |
|
|
|
return; |
|
|
|
} |
|
|
|
this.stack.splice(pos, 1); |
|
|
|
if ( this.stack.length === 0 ) { |
|
|
|
this.destroy(); |
|
|
|
var topEntry = this.stack[count - 1]; |
|
|
|
if ( topEntry && topEntry.url === url ) { |
|
|
|
if ( committed ) { |
|
|
|
topEntry.committed = true; |
|
|
|
} |
|
|
|
return; |
|
|
|
} |
|
|
|
if ( pos !== this.stack.length ) { |
|
|
|
return; |
|
|
|
if ( this.commitTimer !== null ) { |
|
|
|
clearTimeout(this.commitTimer); |
|
|
|
} |
|
|
|
this.update(); |
|
|
|
}; |
|
|
|
|
|
|
|
// This tells that the url is definitely the one to be associated with the
|
|
|
|
// tab, there is no longer any ambiguity about which root URL is really
|
|
|
|
// sitting in which tab.
|
|
|
|
TabContext.prototype.commit = function(url) { |
|
|
|
if ( vAPI.isBehindTheSceneTabId(this.tabId) ) { |
|
|
|
return; |
|
|
|
if ( committed ) { |
|
|
|
this.stack = [new StackEntry(url, true)]; |
|
|
|
} else { |
|
|
|
this.stack.push(new StackEntry(url)); |
|
|
|
this.commitTimer = vAPI.setTimeout(this.onCommit.bind(this), 1000); |
|
|
|
} |
|
|
|
this.stack = [url]; |
|
|
|
this.update(); |
|
|
|
µm.bindTabToPageStats(this.tabId, context); |
|
|
|
}; |
|
|
|
|
|
|
|
// These are to be used for the API of the tab context manager.
|
|
|
|
|
|
|
|
var push = function(tabId, url) { |
|
|
|
var push = function(tabId, url, context) { |
|
|
|
var entry = tabContexts[tabId]; |
|
|
|
if ( entry === undefined ) { |
|
|
|
entry = new TabContext(tabId); |
|
|
|
entry.autodestroy(); |
|
|
|
} |
|
|
|
entry.push(url); |
|
|
|
entry.push(url, context); |
|
|
|
mostRecentRootDocURL = url; |
|
|
|
mostRecentRootDocURLTimestamp = Date.now(); |
|
|
|
return entry; |
|
|
@ -326,23 +336,6 @@ housekeep itself. |
|
|
|
return tabContexts[vAPI.noTabId]; |
|
|
|
}; |
|
|
|
|
|
|
|
var commit = function(tabId, url) { |
|
|
|
var entry = tabContexts[tabId]; |
|
|
|
if ( entry === undefined ) { |
|
|
|
entry = push(tabId, url); |
|
|
|
} else { |
|
|
|
entry.commit(url); |
|
|
|
} |
|
|
|
return entry; |
|
|
|
}; |
|
|
|
|
|
|
|
var unpush = function(tabId, url) { |
|
|
|
var entry = tabContexts[tabId]; |
|
|
|
if ( entry !== undefined ) { |
|
|
|
entry.unpush(url); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
var lookup = function(tabId) { |
|
|
|
return tabContexts[tabId] || null; |
|
|
|
}; |
|
|
@ -350,88 +343,48 @@ housekeep itself. |
|
|
|
// Behind-the-scene tab context
|
|
|
|
(function() { |
|
|
|
var entry = new TabContext(vAPI.noTabId); |
|
|
|
entry.stack.push(''); |
|
|
|
entry.stack.push(new StackEntry('', true)); |
|
|
|
entry.rawURL = ''; |
|
|
|
entry.normalURL = µm.normalizePageURL(entry.tabId); |
|
|
|
entry.rootHostname = µm.URI.hostnameFromURI(entry.normalURL); |
|
|
|
entry.rootDomain = µm.URI.domainFromHostname(entry.rootHostname) || entry.rootHostname; |
|
|
|
})(); |
|
|
|
|
|
|
|
// Context object, typically to be used to feed filtering engines.
|
|
|
|
var Context = function(tabId) { |
|
|
|
var tabContext = lookup(tabId); |
|
|
|
this.rootHostname = tabContext.rootHostname; |
|
|
|
this.rootDomain = tabContext.rootDomain; |
|
|
|
this.pageHostname = |
|
|
|
this.pageDomain = |
|
|
|
this.requestURL = |
|
|
|
this.requestHostname = |
|
|
|
this.requestDomain = ''; |
|
|
|
vAPI.tabs.onNavigation = function(details) { |
|
|
|
var tabId = details.tabId; |
|
|
|
if ( vAPI.isBehindTheSceneTabId(tabId) ) { |
|
|
|
return; |
|
|
|
} |
|
|
|
push(tabId, details.url, 'newURL'); |
|
|
|
}; |
|
|
|
|
|
|
|
vAPI.tabs.onUpdated = function(tabId, changeInfo, tab) { |
|
|
|
if ( typeof tab.url !== 'string' || tab.url === '' ) { |
|
|
|
return; |
|
|
|
} |
|
|
|
if ( vAPI.isBehindTheSceneTabId(tabId) ) { |
|
|
|
return; |
|
|
|
} |
|
|
|
if ( changeInfo.url ) { |
|
|
|
push(tabId, changeInfo.url, 'updateURL'); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
var createContext = function(tabId) { |
|
|
|
return new Context(tabId); |
|
|
|
vAPI.tabs.onClosed = function(tabId) { |
|
|
|
µm.unbindTabFromPageStats(tabId); |
|
|
|
var entry = tabContexts[tabId]; |
|
|
|
if ( entry instanceof TabContext ) { |
|
|
|
entry.destroy(); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
return { |
|
|
|
push: push, |
|
|
|
unpush: unpush, |
|
|
|
commit: commit, |
|
|
|
lookup: lookup, |
|
|
|
mustLookup: mustLookup, |
|
|
|
createContext: createContext |
|
|
|
mustLookup: mustLookup |
|
|
|
}; |
|
|
|
})(); |
|
|
|
|
|
|
|
/******************************************************************************/ |
|
|
|
/******************************************************************************/ |
|
|
|
|
|
|
|
// When the DOM content of root frame is loaded, this means the tab
|
|
|
|
// content has changed.
|
|
|
|
|
|
|
|
vAPI.tabs.onNavigation = function(details) { |
|
|
|
// This actually can happen
|
|
|
|
var tabId = details.tabId; |
|
|
|
if ( vAPI.isBehindTheSceneTabId(tabId) ) { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
//console.log('vAPI.tabs.onNavigation: %s %s %o', details.url, details.transitionType, details.transitionQualifiers);
|
|
|
|
|
|
|
|
µm.tabContextManager.commit(tabId, details.url); |
|
|
|
µm.bindTabToPageStats(tabId, 'commit'); |
|
|
|
}; |
|
|
|
|
|
|
|
/******************************************************************************/ |
|
|
|
|
|
|
|
// It may happen the URL in the tab changes, while the page's document
|
|
|
|
// stays the same (for instance, Google Maps). Without this listener,
|
|
|
|
// the extension icon won't be properly refreshed.
|
|
|
|
|
|
|
|
vAPI.tabs.onUpdated = function(tabId, changeInfo, tab) { |
|
|
|
if ( !tab.url || tab.url === '' ) { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// This actually can happen
|
|
|
|
if ( vAPI.isBehindTheSceneTabId(tabId) ) { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
if ( changeInfo.url ) { |
|
|
|
µm.tabContextManager.commit(tabId, changeInfo.url); |
|
|
|
µm.bindTabToPageStats(tabId, 'updated'); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
/******************************************************************************/ |
|
|
|
|
|
|
|
vAPI.tabs.onClosed = function(tabId) { |
|
|
|
µm.unbindTabFromPageStats(tabId); |
|
|
|
}; |
|
|
|
|
|
|
|
/******************************************************************************/ |
|
|
|
|
|
|
|
vAPI.tabs.registerListeners(); |
|
|
|
|
|
|
|
/******************************************************************************/ |
|
|
@ -466,19 +419,13 @@ vAPI.tabs.registerListeners(); |
|
|
|
return pageStore; |
|
|
|
} |
|
|
|
|
|
|
|
// Do not change anything if it's weak binding -- typically when
|
|
|
|
// binding from network request handler.
|
|
|
|
if ( context === 'weak' ) { |
|
|
|
return pageStore; |
|
|
|
} |
|
|
|
|
|
|
|
// https://github.com/gorhill/uMatrix/issues/37
|
|
|
|
// Just rebind whenever possible: the URL changed, but the document
|
|
|
|
// maybe is the same.
|
|
|
|
// Example: Google Maps, Github
|
|
|
|
// https://github.com/gorhill/uMatrix/issues/72
|
|
|
|
// Need to double-check that the new scope is same as old scope
|
|
|
|
if ( context === 'updated' && pageStore.pageHostname === tabContext.rootHostname ) { |
|
|
|
if ( context === 'updateURL' && pageStore.pageHostname === tabContext.rootHostname ) { |
|
|
|
pageStore.rawURL = tabContext.rawURL; |
|
|
|
pageStore.normalURL = normalURL; |
|
|
|
this.updateTitle(tabId); |
|
|
@ -516,6 +463,7 @@ vAPI.tabs.registerListeners(); |
|
|
|
if ( pageStore === null ) { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
delete this.pageStores[tabId]; |
|
|
|
this.pageStoresToken = Date.now(); |
|
|
|
|
|
|
|