uBlock/js/assets.js

416 lines
13 KiB
JavaScript

/*******************************************************************************
µ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, µBlock */
/*******************************************************************************
Assets
Read:
If in cache
Use cache
If not in cache
Use local
Update:
Use remote
Save in cache
Import:
Use textarea
Save in cache [user directory]
File system structure:
assets
ublock
...
thirdparties
...
user
blacklisted-hosts.txt
...
*/
// Ref: http://www.w3.org/TR/2012/WD-file-system-api-20120417/
// Ref: http://www.html5rocks.com/en/tutorials/file/filesystem/
/******************************************************************************/
// Low-level asset files manager
µBlock.assets = (function() {
/******************************************************************************/
var fileSystem;
var fileSystemQuota = 40 * 1024 * 1024;
var remoteRoot = µBlock.projectServerRoot;
/******************************************************************************/
var nullFunc = function() { };
/******************************************************************************/
var getTextFileFromURL = function(url, onLoad, onError) {
// console.log('µBlock> getTextFileFromURL("%s"):', url);
var xhr = new XMLHttpRequest();
xhr.responseType = 'text';
xhr.onload = onLoad;
xhr.onerror = onError;
xhr.ontimeout = onError;
xhr.open('get', url, true);
xhr.send();
};
/******************************************************************************/
// Useful to avoid having to manage a directory tree
var cachePathFromPath = function(path) {
return path.replace(/\//g, '___');
};
var pathFromCachePath = function(path) {
return path.replace(/___/g, '/');
};
/******************************************************************************/
var requestFileSystem = function(onSuccess, onError) {
if ( fileSystem ) {
onSuccess(fileSystem);
return;
}
var onRequestFileSystem = function(fs) {
fileSystem = fs;
onSuccess(fs);
};
var onRequestQuota = function(grantedBytes) {
window.webkitRequestFileSystem(window.PERSISTENT, grantedBytes, onRequestFileSystem, onError);
};
navigator.webkitPersistentStorage.requestQuota(fileSystemQuota, onRequestQuota, onError);
};
/******************************************************************************/
// Flush cached non-user assets if these are from a prior version.
// https://github.com/gorhill/httpswitchboard/issues/212
var cacheSynchronized = false;
var synchronizeCache = function() {
if ( cacheSynchronized ) {
return;
}
cacheSynchronized = true;
var directoryReader;
var done = function() {
directoryReader = null;
};
var onReadEntries = function(entries) {
var n = entries.length;
if ( !n ) {
return done();
}
var entry;
for ( var i = 0; i < n; i++ ) {
entry = entries[i];
// Ignore whatever is in 'user' folder: these are NOT cached entries.
if ( pathFromCachePath(entry.fullPath).indexOf('/assets/user/') >= 0 ) {
continue;
}
entry.remove(nullFunc);
}
directoryReader.readEntries(onReadEntries, onReadEntriesError);
};
var onReadEntriesError = function(err) {
console.error('µBlock> synchronizeCache() / onReadEntriesError("%s"):', err.name);
done();
};
var onRequestFileSystemSuccess = function(fs) {
directoryReader = fs.root.createReader();
directoryReader.readEntries(onReadEntries, onReadEntriesError);
};
var onRequestFileSystemError = function(err) {
console.error('µBlock> synchronizeCache() / onRequestFileSystemError():', err.name);
done();
};
var onLastVersionRead = function(store) {
var currentVersion = chrome.runtime.getManifest().version;
var lastVersion = store.extensionLastVersion || '0.0.0.0';
if ( currentVersion === lastVersion ) {
return done();
}
chrome.storage.local.set({ 'extensionLastVersion': currentVersion });
requestFileSystem(onRequestFileSystemSuccess, onRequestFileSystemError);
};
chrome.storage.local.get('extensionLastVersion', onLastVersionRead);
};
/******************************************************************************/
var readLocalFile = function(path, callback) {
var reportBack = function(content, err) {
var details = {
'path': path,
'content': content,
'error': err
};
callback(details);
};
var onLocalFileLoaded = function() {
// console.log('µBlock> onLocalFileLoaded()');
reportBack(this.responseText);
this.onload = this.onerror = null;
};
var onLocalFileError = function(ev) {
console.error('µBlock> readLocalFile() / onLocalFileError("%s")', path);
reportBack('', 'Error');
this.onload = this.onerror = null;
};
var onCacheFileLoaded = function() {
// console.log('µBlock> readLocalFile() / onCacheFileLoaded()');
reportBack(this.responseText);
this.onload = this.onerror = null;
};
var onCacheFileError = function(ev) {
// This handler may be called under normal circumstances: it appears
// the entry may still be present even after the file was removed.
// console.error('µBlock> readLocalFile() / onCacheFileError("%s")', path);
getTextFileFromURL(chrome.runtime.getURL(path), onLocalFileLoaded, onLocalFileError);
this.onload = this.onerror = null;
};
var onCacheEntryFound = function(entry) {
// console.log('µBlock> readLocalFile() / onCacheEntryFound():', entry.toURL());
// rhill 2014-04-18: `ublock` query parameter is added to ensure
// the browser cache is bypassed.
getTextFileFromURL(entry.toURL() + '?ublock=' + Date.now(), onCacheFileLoaded, onCacheFileError);
};
var onCacheEntryError = function(err) {
if ( err.name !== 'NotFoundError' ) {
console.error('µBlock> readLocalFile() / onCacheEntryError("%s"):', path, err.name);
}
getTextFileFromURL(chrome.runtime.getURL(path), onLocalFileLoaded, onLocalFileError);
};
var onRequestFileSystemSuccess = function(fs) {
fs.root.getFile(cachePathFromPath(path), null, onCacheEntryFound, onCacheEntryError);
};
var onRequestFileSystemError = function(err) {
console.error('µBlock> readLocalFile() / onRequestFileSystemError():', err.name);
getTextFileFromURL(chrome.runtime.getURL(path), onLocalFileLoaded, onLocalFileError);
};
requestFileSystem(onRequestFileSystemSuccess, onRequestFileSystemError);
};
/******************************************************************************/
var readRemoteFile = function(path, callback) {
var reportBack = function(content, err) {
var details = {
'path': path,
'content': content,
'error': err
};
callback(details);
};
var onRemoteFileLoaded = function() {
// console.log('µBlock> readRemoteFile() / onRemoteFileLoaded()');
// https://github.com/gorhill/httpswitchboard/issues/263
if ( this.status === 200 ) {
reportBack(this.responseText);
} else {
reportBack('', 'Error ' + this.statusText);
}
this.onload = this.onerror = null;
};
var onRemoteFileError = function(ev) {
console.error('µBlock> readRemoteFile() / onRemoteFileError("%s")', path);
reportBack('', 'Error');
this.onload = this.onerror = null;
};
// 'ublock=...' is to skip browser cache
getTextFileFromURL(
remoteRoot + path + '?ublock=' + Date.now(),
onRemoteFileLoaded,
onRemoteFileError
);
};
/******************************************************************************/
var writeLocalFile = function(path, content, callback) {
var reportBack = function(err) {
var details = {
'path': path,
'content': content,
'error': err
};
callback(details);
};
var onFileWriteSuccess = function() {
// console.log('µBlock> writeLocalFile() / onFileWriteSuccess("%s")', path);
reportBack();
};
var onFileWriteError = function(err) {
console.error('µBlock> writeLocalFile() / onFileWriteError("%s"):', path, err.name);
reportBack(err.name);
};
var onFileTruncateSuccess = function() {
// console.log('µBlock> writeLocalFile() / onFileTruncateSuccess("%s")', path);
this.onwriteend = onFileWriteSuccess;
this.onerror = onFileWriteError;
var blob = new Blob([content], { type: 'text/plain' });
this.write(blob);
};
var onFileTruncateError = function(err) {
console.error('µBlock> writeLocalFile() / onFileTruncateError("%s"):', path, err.name);
reportBack(err.name);
};
var onCreateFileWriterSuccess = function(fwriter) {
fwriter.onwriteend = onFileTruncateSuccess;
fwriter.onerror = onFileTruncateError;
fwriter.truncate(0);
};
var onCreateFileWriterError = function(err) {
console.error('µBlock> writeLocalFile() / onCreateFileWriterError("%s"):', path, err.name);
reportBack(err.name);
};
var onCacheEntryFound = function(file) {
// console.log('µBlock> writeLocalFile() / onCacheEntryFound():', file.toURL());
file.createWriter(onCreateFileWriterSuccess, onCreateFileWriterError);
};
var onCacheEntryError = function(err) {
console.error('µBlock> writeLocalFile() / onCacheEntryError("%s"):', path, err.name);
reportBack(err.name);
};
var onRequestFileSystemError = function(err) {
console.error('µBlock> writeLocalFile() / onRequestFileSystemError():', err.name);
reportBack(err.name);
};
var onRequestFileSystem = function(fs) {
fs.root.getFile(cachePathFromPath(path), { create: true }, onCacheEntryFound, onCacheEntryError);
};
requestFileSystem(onRequestFileSystem, onRequestFileSystemError);
};
/******************************************************************************/
var updateFromRemote = function(details, callback) {
// 'ublock=...' is to skip browser cache
var remoteURL = remoteRoot + details.path + '?ublock=' + Date.now();
var targetPath = details.path;
var targetMd5 = details.md5 || '';
var reportBackError = function() {
callback({
'path': targetPath,
'error': 'Error'
});
};
var onRemoteFileLoaded = function() {
this.onload = this.onerror = null;
if ( typeof this.responseText !== 'string' ) {
console.error('µBlock> updateFromRemote("%s") / onRemoteFileLoaded(): no response', remoteURL);
reportBackError();
return;
}
if ( YaMD5.hashStr(this.responseText) !== targetMd5 ) {
console.error('µBlock> updateFromRemote("%s") / onRemoteFileLoaded(): bad md5 checksum', remoteURL);
reportBackError();
return;
}
// console.debug('µBlock> updateFromRemote("%s") / onRemoteFileLoaded()', remoteURL);
writeLocalFile(targetPath, this.responseText, callback);
};
var onRemoteFileError = function(ev) {
this.onload = this.onerror = null;
console.error('µBlock> updateFromRemote() / onRemoteFileError("%s"):', remoteURL, this.statusText);
reportBackError();
};
getTextFileFromURL(
remoteURL,
onRemoteFileLoaded,
onRemoteFileError
);
};
/******************************************************************************/
// Flush cached assets if cache content is from an older version: the extension
// always ships with the most up-to-date assets.
synchronizeCache();
/******************************************************************************/
// Export API
return {
'get': readLocalFile,
'getRemote': readRemoteFile,
'put': writeLocalFile,
'update': updateFromRemote
};
/******************************************************************************/
})();
/******************************************************************************/