251 lines
12 KiB
JavaScript
251 lines
12 KiB
JavaScript
/** Wrap an API that uses callbacks with Promises
|
|
* This expects the pattern function withCallback(arg1, arg2, ... argN, callback)
|
|
* @author Keith Henry <keith.henry@evolutionjobs.co.uk>
|
|
* @license MIT */
|
|
(function () {
|
|
|
|
// before we start: don't do shit in browsers that aren't Google Chrome.
|
|
// We might need to modify this for use in IE at a later date tho
|
|
if(chrome === undefined)
|
|
return;
|
|
|
|
|
|
'use strict';
|
|
|
|
/** Wrap a function with a callback with a Promise.
|
|
* @param {function} f The function to wrap, should be pattern: withCallback(arg1, arg2, ... argN, callback).
|
|
* @param {function} parseCB Optional function to parse multiple callback parameters into a single object.
|
|
* @returns {Promise} Promise that resolves when the callback fires. */
|
|
function promisify(f, parseCB) {
|
|
return (...args) => {
|
|
let safeArgs = args;
|
|
let callback;
|
|
// The Chrome API functions all use arguments, so we can't use f.length to check
|
|
|
|
// If there is a last arg
|
|
if (args && args.length > 0) {
|
|
|
|
// ... and the last arg is a function
|
|
const last = args[args.length - 1];
|
|
if (typeof last === 'function') {
|
|
// Trim the last callback arg if it's been passed
|
|
safeArgs = args.slice(0, args.length - 1);
|
|
callback = last;
|
|
}
|
|
}
|
|
|
|
// Return a promise
|
|
return new Promise((resolve, reject) => {
|
|
try {
|
|
// Try to run the original function, with the trimmed args list
|
|
f(...safeArgs, (...cbArgs) => {
|
|
|
|
// If a callback was passed at the end of the original arguments
|
|
if (callback) {
|
|
// Don't allow a bug in the callback to stop the promise resolving
|
|
try { callback(...cbArgs); }
|
|
catch (cbErr) { reject(cbErr); }
|
|
}
|
|
|
|
// Chrome extensions always fire the callback, but populate chrome.runtime.lastError with exception details
|
|
if (chrome.runtime.lastError)
|
|
// Return as an error for the awaited catch block
|
|
reject(new Error(chrome.runtime.lastError.message || `Error thrown by API ${chrome.runtime.lastError}`));
|
|
else {
|
|
if (parseCB) {
|
|
const cbObj = parseCB(...cbArgs);
|
|
resolve(cbObj);
|
|
}
|
|
else if (!cbArgs || cbArgs.length === 0)
|
|
resolve();
|
|
else if (cbArgs.length === 1)
|
|
resolve(cbArgs[0]);
|
|
else
|
|
resolve(cbArgs);
|
|
}
|
|
});
|
|
}
|
|
catch (err) { reject(err); }
|
|
});
|
|
}
|
|
}
|
|
|
|
/** Promisify all the known functions in the map
|
|
* @param {object} api The Chrome native API to extend
|
|
* @param {Array} apiMap Collection of sub-API and functions to promisify */
|
|
function applyMap(api, apiMap) {
|
|
if (!api)
|
|
// Not supported by current permissions
|
|
return;
|
|
|
|
for (let funcDef of apiMap) {
|
|
let funcName;
|
|
if (typeof funcDef === 'string')
|
|
funcName = funcDef;
|
|
else {
|
|
funcName = funcDef.n;
|
|
}
|
|
|
|
if (!api.hasOwnProperty(funcName))
|
|
// Member not in API
|
|
continue;
|
|
|
|
const m = api[funcName];
|
|
if (typeof m === 'function')
|
|
// This is a function, wrap in a promise
|
|
api[funcName] = promisify(m, funcDef.cb);
|
|
else
|
|
// Sub-API, recurse this func with the mapped props
|
|
applyMap(m, funcDef.props);
|
|
}
|
|
}
|
|
|
|
/** Apply promise-maps to the Chrome native API.
|
|
* @param {object} apiMaps The API to apply. */
|
|
function applyMaps(apiMaps) {
|
|
for (let apiName in apiMaps) {
|
|
const callbackApi = chrome[apiName];
|
|
if (!callbackApi)
|
|
// Not supported by current permissions
|
|
continue;
|
|
|
|
const apiMap = apiMaps[apiName];
|
|
applyMap(callbackApi, apiMap);
|
|
}
|
|
}
|
|
|
|
// accessibilityFeatures https://developer.chrome.com/extensions/accessibilityFeatures
|
|
const knownA11ySetting = ['get', 'set', 'clear'];
|
|
|
|
// ContentSetting https://developer.chrome.com/extensions/contentSettings#type-ContentSetting
|
|
const knownInContentSetting = ['clear', 'get', 'set', 'getResourceIdentifiers'];
|
|
|
|
// StorageArea https://developer.chrome.com/extensions/storage#type-StorageArea
|
|
const knownInStorageArea = ['get', 'getBytesInUse', 'set', 'remove', 'clear'];
|
|
|
|
/** Map of API functions that follow the callback pattern that we can 'promisify' */
|
|
applyMaps({
|
|
accessibilityFeatures: [ // Todo: this should extend AccessibilityFeaturesSetting.prototype instead
|
|
{ n: 'spokenFeedback', props: knownA11ySetting },
|
|
{ n: 'largeCursor', props: knownA11ySetting },
|
|
{ n: 'stickyKeys', props: knownA11ySetting },
|
|
{ n: 'highContrast', props: knownA11ySetting },
|
|
{ n: 'screenMagnifier', props: knownA11ySetting },
|
|
{ n: 'autoclick', props: knownA11ySetting },
|
|
{ n: 'virtualKeyboard', props: knownA11ySetting },
|
|
{ n: 'animationPolicy', props: knownA11ySetting }],
|
|
alarms: ['get', 'getAll', 'clear', 'clearAll'],
|
|
bookmarks: [
|
|
'get', 'getChildren', 'getRecent', 'getTree', 'getSubTree',
|
|
'search', 'create', 'move', 'update', 'remove', 'removeTree'],
|
|
browser: ['openTab'],
|
|
browserAction: [
|
|
'getTitle', 'setIcon', 'getPopup', 'getBadgeText', 'getBadgeBackgroundColor'],
|
|
browsingData: [
|
|
'settings', 'remove', 'removeAppcache', 'removeCache',
|
|
'removeCookies', 'removeDownloads', 'removeFileSystems',
|
|
'removeFormData', 'removeHistory', 'removeIndexedDB',
|
|
'removeLocalStorage', 'removePluginData', 'removePasswords',
|
|
'removeWebSQL'],
|
|
commands: ['getAll'],
|
|
contentSettings: [ // Todo: this should extend ContentSetting.prototype instead
|
|
{ n: 'cookies', props: knownInContentSetting },
|
|
{ n: 'images', props: knownInContentSetting },
|
|
{ n: 'javascript', props: knownInContentSetting },
|
|
{ n: 'location', props: knownInContentSetting },
|
|
{ n: 'plugins', props: knownInContentSetting },
|
|
{ n: 'popups', props: knownInContentSetting },
|
|
{ n: 'notifications', props: knownInContentSetting },
|
|
{ n: 'fullscreen', props: knownInContentSetting },
|
|
{ n: 'mouselock', props: knownInContentSetting },
|
|
{ n: 'microphone', props: knownInContentSetting },
|
|
{ n: 'camera', props: knownInContentSetting },
|
|
{ n: 'unsandboxedPlugins', props: knownInContentSetting },
|
|
{ n: 'automaticDownloads', props: knownInContentSetting }],
|
|
contextMenus: ['create', 'update', 'remove', 'removeAll'],
|
|
cookies: ['get', 'getAll', 'set', 'remove', 'getAllCookieStores'],
|
|
debugger: ['attach', 'detach', 'sendCommand', 'getTargets'],
|
|
desktopCapture: ['chooseDesktopMedia'],
|
|
// TODO: devtools.*
|
|
documentScan: ['scan'],
|
|
downloads: [
|
|
'download', 'search', 'pause', 'resume', 'cancel',
|
|
'getFileIcon', 'erase', 'removeFile', 'acceptDanger'],
|
|
enterprise: [{ n: 'platformKeys', props: ['getToken', 'getCertificates', 'importCertificate', 'removeCertificate'] }],
|
|
extension: ['isAllowedIncognitoAccess', 'isAllowedFileSchemeAccess'], // mostly deprecated in favour of runtime
|
|
fileBrowserHandler: ['selectFile'],
|
|
fileSystemProvider: ['mount', 'unmount', 'getAll', 'get', 'notify'],
|
|
fontSettings: [
|
|
'setDefaultFontSize', 'getFont', 'getDefaultFontSize', 'getMinimumFontSize',
|
|
'setMinimumFontSize', 'getDefaultFixedFontSize', 'clearDefaultFontSize',
|
|
'setDefaultFixedFontSize', 'clearFont', 'setFont', 'clearMinimumFontSize',
|
|
'getFontList', 'clearDefaultFixedFontSize'],
|
|
gcm: ['register', 'unregister', 'send'],
|
|
history: ['search', 'getVisits', 'addUrl', 'deleteUrl', 'deleteRange', 'deleteAll'],
|
|
i18n: ['getAcceptLanguages', 'detectLanguage'],
|
|
identity: [
|
|
'getAuthToken', 'getProfileUserInfo', 'removeCachedAuthToken',
|
|
'launchWebAuthFlow', 'getRedirectURL'],
|
|
idle: ['queryState'],
|
|
input: [{
|
|
n: 'ime', props: [
|
|
'setMenuItems', 'commitText', 'setCandidates', 'setComposition', 'updateMenuItems',
|
|
'setCandidateWindowProperties', 'clearComposition', 'setCursorPosition', 'sendKeyEvents',
|
|
'deleteSurroundingText']
|
|
}],
|
|
management: [
|
|
'setEnabled', 'getPermissionWarningsById', 'get', 'getAll',
|
|
'getPermissionWarningsByManifest', 'launchApp', 'uninstall', 'getSelf',
|
|
'uninstallSelf', 'createAppShortcut', 'setLaunchType', 'generateAppForLink'],
|
|
networking: [{ n: 'config', props: ['setNetworkFilter', 'finishAuthentication'] }],
|
|
notifications: ['create', 'update', 'clear', 'getAll', 'getPermissionLevel'],
|
|
pageAction: ['getTitle', 'setIcon', 'getPopup'],
|
|
pageCapture: ['saveAsMHTML'],
|
|
permissions: ['getAll', 'contains', 'request', 'remove'],
|
|
platformKeys: ['selectClientCertificates', 'verifyTLSServerCertificate',
|
|
{ n: "getKeyPair", cb: (publicKey, privateKey) => { return { publicKey, privateKey }; } }],
|
|
runtime: [
|
|
'getBackgroundPage', 'openOptionsPage', 'setUninstallURL',
|
|
'restartAfterDelay', 'sendMessage',
|
|
'sendNativeMessage', 'getPlatformInfo', 'getPackageDirectoryEntry',
|
|
{ n: "requestUpdateCheck", cb: (status, details) => { return { status, details }; } }],
|
|
scriptBadge: ['getPopup'],
|
|
sessions: ['getRecentlyClosed', 'getDevices', 'restore'],
|
|
storage: [ // Todo: this should extend StorageArea.prototype instead
|
|
{ n: 'sync', props: knownInStorageArea },
|
|
{ n: 'local', props: knownInStorageArea },
|
|
{ n: 'managed', props: knownInStorageArea }],
|
|
socket: [
|
|
'create', 'connect', 'bind', 'read', 'write', 'recvFrom', 'sendTo',
|
|
'listen', 'accept', 'setKeepAlive', 'setNoDelay', 'getInfo', 'getNetworkList'],
|
|
sockets: [
|
|
{ n: 'tcp', props: [
|
|
'create','update','setPaused','setKeepAlive','setNoDelay','connect',
|
|
'disconnect','secure','send','close','getInfo','getSockets'] },
|
|
{ n: 'tcpServer', props: [
|
|
'create','update','setPaused','listen','disconnect','close','getInfo','getSockets'] },
|
|
{ n: 'udp', props: [
|
|
'create','update','setPaused','bind','send','close','getInfo',
|
|
'getSockets','joinGroup','leaveGroup','setMulticastTimeToLive',
|
|
'setMulticastLoopbackMode','getJoinedGroups','setBroadcast'] }],
|
|
system: [
|
|
{ n: 'cpu', props: ['getInfo'] },
|
|
{ n: 'memory', props: ['getInfo'] },
|
|
{ n: 'storage', props: ['getInfo', 'ejectDevice', 'getAvailableCapacity'] }],
|
|
tabCapture: ['capture', 'getCapturedTabs'],
|
|
tabs: [
|
|
'get', 'getCurrent', 'sendMessage', 'create', 'duplicate',
|
|
'query', 'highlight', 'update', 'move', 'reload', 'remove',
|
|
'detectLanguage', 'captureVisibleTab', 'executeScript',
|
|
'insertCSS', 'setZoom', 'getZoom', 'setZoomSettings',
|
|
'getZoomSettings', 'discard'],
|
|
topSites: ['get'],
|
|
tts: ['isSpeaking', 'getVoices', 'speak'],
|
|
types: ['set', 'get', 'clear'],
|
|
vpnProvider: ['createConfig', 'destroyConfig', 'setParameters', 'sendPacket', 'notifyConnectionStateChanged'],
|
|
wallpaper: ['setWallpaper'],
|
|
webNavigation: ['getFrame', 'getAllFrames', 'handlerBehaviorChanged'],
|
|
windows: ['get', 'getCurrent', 'getLastFocused', 'getAll', 'create', 'update', 'remove']
|
|
});
|
|
})();
|