Addon.sys.mjs (7390B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 const lazy = {}; 6 7 ChromeUtils.defineESModuleGetters(lazy, { 8 AddonManager: "resource://gre/modules/AddonManager.sys.mjs", 9 ExtensionPermissions: "resource://gre/modules/ExtensionPermissions.sys.mjs", 10 FileUtils: "resource://gre/modules/FileUtils.sys.mjs", 11 12 AppInfo: "chrome://remote/content/shared/AppInfo.sys.mjs", 13 error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs", 14 generateUUID: "chrome://remote/content/shared/UUID.sys.mjs", 15 }); 16 17 // from https://developer.mozilla.org/en-US/Add-ons/Add-on_Manager/AddonManager#AddonInstall_errors 18 const ERRORS = { 19 [-1]: "ERROR_NETWORK_FAILURE: A network error occurred.", 20 [-2]: "ERROR_INCORRECT_HASH: The downloaded file did not match the expected hash.", 21 [-3]: "ERROR_CORRUPT_FILE: The file appears to be corrupt.", 22 [-4]: "ERROR_FILE_ACCESS: There was an error accessing the filesystem.", 23 [-5]: "ERROR_SIGNEDSTATE_REQUIRED: The addon must be signed and isn't.", 24 [-6]: "ERROR_UNEXPECTED_ADDON_TYPE: The downloaded add-on had a different type than expected (during an update).", 25 [-7]: "ERROR_INCORRECT_ID: The addon did not have the expected ID (during an update).", 26 [-8]: "ERROR_INVALID_DOMAIN: The addon install_origins does not list the 3rd party domain.", 27 [-9]: "ERROR_UNEXPECTED_ADDON_VERSION: The downloaded add-on had a different version than expected (during an update).", 28 [-10]: "ERROR_BLOCKLISTED: The add-on is blocklisted.", 29 [-11]: 30 "ERROR_INCOMPATIBLE: The add-on is incompatible (w.r.t. the compatibility range).", 31 [-12]: 32 "ERROR_UNSUPPORTED_ADDON_TYPE: The add-on type is not supported by the platform.", 33 }; 34 35 async function installAddon(file, temporary, allowPrivateBrowsing) { 36 let addon; 37 try { 38 if (temporary) { 39 addon = await lazy.AddonManager.installTemporaryAddon(file); 40 } else { 41 const install = await lazy.AddonManager.getInstallForFile(file, null, { 42 source: "internal", 43 }); 44 45 if (install == null) { 46 throw new lazy.error.UnknownError("Unknown error"); 47 } 48 49 try { 50 addon = await install.install(); 51 } catch { 52 throw new lazy.error.UnknownError(ERRORS[install.error]); 53 } 54 } 55 } catch (e) { 56 throw new lazy.error.InvalidWebExtensionError( 57 `Could not install Add-on: ${e.message}` 58 ); 59 } 60 61 if (allowPrivateBrowsing) { 62 const perms = { 63 permissions: ["internal:privateBrowsingAllowed"], 64 origins: [], 65 }; 66 await lazy.ExtensionPermissions.add(addon.id, perms); 67 await addon.reload(); 68 } 69 70 return addon; 71 } 72 73 /** Installs addons by path and uninstalls by ID. */ 74 export class Addon { 75 /** 76 * Install a Firefox addon with provided base64 string representation. 77 * 78 * Temporary addons will automatically be uninstalled on shutdown and 79 * do not need to be signed, though they must be restartless. 80 * 81 * @param {string} base64 82 * Base64 string representation of the extension package archive. 83 * @param {boolean=} temporary 84 * True to install the addon temporarily, false (default) otherwise. 85 * @param {boolean=} allowPrivateBrowsing 86 * True to install the addon that is enabled in Private Browsing mode, 87 * false (default) otherwise. 88 * 89 * @returns {Promise.<string>} 90 * Addon ID. 91 * 92 * @throws {UnknownError} 93 * If there is a problem installing the addon. 94 */ 95 static async installWithBase64(base64, temporary, allowPrivateBrowsing) { 96 const decodedString = atob(base64); 97 const fileContent = Uint8Array.from(decodedString, m => m.codePointAt(0)); 98 99 let path; 100 try { 101 path = PathUtils.join( 102 PathUtils.profileDir, 103 `addon-test-${lazy.generateUUID()}.xpi` 104 ); 105 await IOUtils.write(path, fileContent); 106 } catch (e) { 107 throw new lazy.error.UnknownError( 108 `Could not write add-on to file: ${e.message}`, 109 e 110 ); 111 } 112 113 let addon; 114 try { 115 const file = new lazy.FileUtils.File(path); 116 addon = await installAddon(file, temporary, allowPrivateBrowsing); 117 } finally { 118 await IOUtils.remove(path); 119 } 120 121 return addon.id; 122 } 123 124 /** 125 * Install a Firefox addon with provided path. 126 * 127 * Temporary addons will automatically be uninstalled on shutdown and 128 * do not need to be signed, though they must be restartless. 129 * 130 * @param {string} path 131 * Full path to the extension package archive. 132 * @param {boolean=} temporary 133 * True to install the addon temporarily, false (default) otherwise. 134 * @param {boolean=} allowPrivateBrowsing 135 * True to install the addon that is enabled in Private Browsing mode, 136 * false (default) otherwise. 137 * 138 * @returns {Promise.<string>} 139 * Addon ID. 140 * 141 * @throws {UnknownError} 142 * If there is a problem installing the addon. 143 */ 144 static async installWithPath(path, temporary, allowPrivateBrowsing) { 145 let file; 146 147 // On Windows we can end up with a path with mixed \ and / 148 // which doesn't work in Firefox. 149 if (lazy.AppInfo.isWindows) { 150 path = path.replace(/\//g, "\\"); 151 } 152 153 try { 154 file = new lazy.FileUtils.File(path); 155 } catch (e) { 156 throw new lazy.error.UnknownError(`Expected absolute path: ${e}`, e); 157 } 158 159 if (!file.exists()) { 160 throw new lazy.error.UnknownError(`No such file or directory: ${path}`); 161 } 162 163 const addon = await installAddon(file, temporary, allowPrivateBrowsing); 164 165 return addon.id; 166 } 167 168 /** 169 * Uninstall a Firefox addon. 170 * 171 * If the addon is restartless it will be uninstalled right away. 172 * Otherwise, Firefox must be restarted for the change to take effect. 173 * 174 * @param {string} id 175 * ID of the addon to uninstall. 176 * 177 * @returns {Promise} 178 * 179 * @throws {UnknownError} 180 * If there is a problem uninstalling the addon. 181 * @throws {NoSuchWebExtensionError} 182 * Raised if the WebExtension with provided id could not be found. 183 */ 184 static async uninstall(id) { 185 let candidate = await lazy.AddonManager.getAddonByID(id); 186 if (candidate === null) { 187 // `AddonManager.getAddonByID` never rejects but instead 188 // returns `null` if the requested addon cannot be found. 189 throw new lazy.error.NoSuchWebExtensionError( 190 `Add-on with ID "${id}" is not installed.` 191 ); 192 } 193 194 return new Promise((resolve, reject) => { 195 let listener = { 196 onOperationCancelled: addon => { 197 if (addon.id === candidate.id) { 198 lazy.AddonManager.removeAddonListener(listener); 199 throw new lazy.error.UnknownError( 200 `Uninstall of Add-on with ID "${candidate.id}" was canceled.` 201 ); 202 } 203 }, 204 205 onUninstalled: addon => { 206 if (addon.id === candidate.id) { 207 lazy.AddonManager.removeAddonListener(listener); 208 resolve(); 209 } 210 }, 211 }; 212 213 lazy.AddonManager.addAddonListener(listener); 214 candidate.uninstall().catch(e => { 215 lazy.AddonManager.removeAddonListener(listener); 216 reject( 217 new lazy.error.UnknownError( 218 `Failed to uninstall Add-on with ID "${id}": ${e.message}` 219 ) 220 ); 221 }); 222 }); 223 } 224 }