/******************************************************************************* µBlock - a Chromium browser extension to block requests. Copyright (C) 2014 Raymond Hill This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see {http://www.gnu.org/licenses/}. Home: https://github.com/gorhill/uBlock */ /* global chrome, YaMD5, µBlock */ /******************************************************************************/ // Low-level asset files manager µBlock.mirrors = (function() { /******************************************************************************/ var exports = { bytesInUse: 0, hitCount: 0 }; /******************************************************************************/ var nullFunc = function() {}; var mirrorCandidates = { 'ajax.googleapis.com': /^ajax\.googleapis\.com\/ajax\/libs\//, 'fonts.googleapis.com': /^fonts\.googleapis\.com/, 'fonts.gstatic.com': /^fonts\.gstatic\.com/, 'cdnjs.cloudflare.com': /^cdnjs\.cloudflare\.com\/ajax\/libs\//, 'code.jquery.com': /^code\.jquery\.com/, 's0.2mdn.net': /(2mdn\.net\/instream\/html5\/ima3\.js)/, 'connect.facebook.net': /(connect\.facebook\.net\/[^\/]+\/all\.js)/, 'www.googletagservices.com': /(www\.googletagservices\.com\/tag\/js\/gpt\.js)/ }; var magicId = 'rmwwgwkzcgfv'; var bytesInUseMax = 20 * 1024 * 1024; var ttl = 30 * 24 * 60 * 60 * 1000; var metadataPersistTimer = null; var metadata = { magicId: magicId, urlKeyToHashMap: {} }; // Hash to content map var hashToDataUrlMap = {}; var loaded = false; /******************************************************************************/ var MetadataEntry = function(hash) { this.accessTime = Date.now(); this.hash = hash; }; var ContentEntry = function(dataURL) { this.createTime = Date.now(); this.dataURL = dataURL; }; /******************************************************************************/ var getTextFileFromURL = function(url, onLoad, onError) { if ( typeof onLoad !== 'function' ) { onLoad = nullFunc; } if ( typeof onError !== 'function' ) { onError = onLoad; } var xhr = new XMLHttpRequest(); xhr.responseType = 'text'; xhr.timeout = 10000; xhr.onload = onLoad; xhr.onerror = onError; xhr.ontimeout = onError; xhr.open('get', url, true); xhr.send(); }; /******************************************************************************/ // Extract a `key` from a URL. var toUrlKey = function(url) { if ( url.slice(0, 4) !== 'http' ) { return ''; } var pos = url.indexOf('://'); if ( pos === -1 ) { return ''; } url = url.slice(pos + 3); pos = url.indexOf('/'); if ( pos === -1 ) { return -1; } var re = mirrorCandidates[url.slice(0, pos)]; if ( typeof re !== 'object' || typeof re.test !== 'function' ) { return ''; } var matches = re.exec(url); if ( matches === null ) { return ''; } return matches.length === 1 ? url : matches[1]; }; /******************************************************************************/ // Ref: http://www.iana.org/assignments/media-types/media-types.xhtml var normalizeContentType = function(ctin) { var ctout; var encoding; var pos = ctin.indexOf(';'); if ( pos === -1 ) { ctout = ctin.trim(); encoding = ''; } else { ctout = ctin.slice(0, pos).trim(); encoding = ctin.slice(pos + 1).trim(); } if ( encoding !== '' ) { ctout += ';' + encoding; } return ctout; }; /******************************************************************************/ var metadataExists = function(urlKey) { return typeof urlKey === 'string' && metadata.urlKeyToHashMap.hasOwnProperty(urlKey); }; /******************************************************************************/ var contentExists = function(hash) { return typeof hash === 'string' && hashToDataUrlMap.hasOwnProperty(hash); }; /******************************************************************************/ var updateMetadata = function() { metadataPersistTimer = null; chrome.storage.local.set({ 'mirrors_metadata': metadata }); }; /******************************************************************************/ var updateMetadataAsync = function(urlKey, hash) { var doesExist = metadataExists(urlKey); if ( doesExist ) { metadata.urlKeyToHashMap[urlKey].accessTime = Date.now(); if ( metadataPersistTimer === null ) { setTimeout(updateMetadata, 60 * 1000); } return; } metadata.urlKeyToHashMap[urlKey] = new MetadataEntry(hash); if ( metadataPersistTimer !== null ) { clearTimeout(metadataPersistTimer); } updateMetadata(); }; /******************************************************************************/ var updateContent = function(hash, dataURL) { if ( contentExists(hash) !== false ) { return; } var contentEntry = hashToDataUrlMap[hash] = new ContentEntry(dataURL); exports.bytesInUse += dataURL.length; var key = 'mirrors_item_' + hash; var bin = {}; bin[key] = contentEntry; chrome.storage.local.set(bin); }; /******************************************************************************/ var cacheAsset = function(url) { var urlKey = toUrlKey(url); if ( metadataExists(urlKey) ) { return; } var onRemoteAssetLoaded = function() { this.onload = this.onerror = null; if ( this.status !== 200 ) { return; } var contentType = normalizeContentType(this.getResponseHeader('Content-Type')); if ( contentType === '' ) { //console.debug('mirrors.cacheAsset(): no good content type available'); return; } var yamd5 = new YaMD5(); yamd5.appendAsciiStr(contentType); yamd5.appendAsciiStr(this.response); var hash = yamd5.end(); updateMetadataAsync(urlKey, hash); if ( contentExists(hash) ) { //console.debug('mirrors.cacheAsset(): reusing existing content for "%s"', urlKey); return; } //console.debug('mirrors.cacheAsset(): caching new content for "%s"', urlKey); // Keep original encoding if there was one, otherwise use base64 -- // as the result is somewhat more compact I believe var dataUrl = contentType.indexOf(';') !== -1 ? 'data:' + contentType + ',' + encodeURIComponent(this.responseText) : 'data:' + contentType + ';base64,' + btoa(this.response); updateContent(hash, dataUrl); }; var onRemoteAssetError = function() { this.onload = this.onerror = null; }; getTextFileFromURL( url, onRemoteAssetLoaded, onRemoteAssetError ); }; /******************************************************************************/ var toURL = function(url, cache) { var urlKey = toUrlKey(url); if ( urlKey === '' ) { return ''; } if ( metadataExists(urlKey) === false ) { if ( cache === true ) { cacheAsset(url); } return ''; } var metadataEntry = metadata.urlKeyToHashMap[urlKey]; if ( contentExists(metadataEntry.hash) === false ) { return ''; } var contentEntry = hashToDataUrlMap[metadataEntry.hash]; updateMetadataAsync(urlKey); exports.hitCount += 1; return contentEntry.dataURL; }; /******************************************************************************/ var load = function() { loaded = true; var loadContent = function(hash) { var key = 'mirrors_item_' + hash; var onContentReady = function(bin) { if ( chrome.runtime.lastError ) { return; } var contentEntry = bin[key]; hashToDataUrlMap[hash] = contentEntry; exports.bytesInUse += contentEntry.dataURL.length; }; var bin = {}; bin[key] = ''; chrome.storage.local.get(bin, onContentReady); }; var onMetadataReady = function(bin) { if ( chrome.runtime.lastError ) { return; } metadata = bin.mirrors_metadata; var hemap = metadata.urlKeyToHashMap; for ( var urlKey in hemap ) { if ( hemap.hasOwnProperty(urlKey) === false ) { continue; } loadContent(hemap[urlKey].hash); } }; chrome.storage.local.get({ 'mirrors_metadata' : metadata }, onMetadataReady); }; /******************************************************************************/ var unload = function() { if ( metadataPersistTimer !== null ) { updateMetadata(); } metadata.urlKeyToHashMap = {}; hashToDataUrlMap = {}; exports.bytesInUse = 0; exports.hitCount = 0; loaded = false; }; /******************************************************************************/ exports.toggle = function(on) { if ( on && loaded !== true ) { load(); } else if ( on !== true && loaded ) { unload(); } }; /******************************************************************************/ // Export API exports.toURL = toURL; return exports; /******************************************************************************/ })(); /******************************************************************************/