ultrawidify/js/dep/chrome/chrome-extension-async.js

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']
});
})();