summaryrefslogtreecommitdiffstats
path: root/remote/marionette/addon.sys.mjs
blob: 5d7169444c2c5e75e8f116c1feb33b0751a19047 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
  FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
  error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
});

// from https://developer.mozilla.org/en-US/Add-ons/Add-on_Manager/AddonManager#AddonInstall_errors
const ERRORS = {
  [-1]: "ERROR_NETWORK_FAILURE: A network error occured.",
  [-2]: "ERROR_INCORRECT_HASH: The downloaded file did not match the expected hash.",
  [-3]: "ERROR_CORRUPT_FILE: The file appears to be corrupt.",
  [-4]: "ERROR_FILE_ACCESS: There was an error accessing the filesystem.",
  [-5]: "ERROR_SIGNEDSTATE_REQUIRED: The addon must be signed and isn't.",
  [-6]: "ERROR_UNEXPECTED_ADDON_TYPE: The downloaded add-on had a different type than expected (during an update).",
  [-7]: "ERROR_INCORRECT_ID: The addon did not have the expected ID (during an update).",
  [-8]: "ERROR_INVALID_DOMAIN: The addon install_origins does not list the 3rd party domain.",
  [-9]: "ERROR_UNEXPECTED_ADDON_VERSION: The downloaded add-on had a different version than expected (during an update).",
  [-10]: "ERROR_BLOCKLISTED: The add-on is blocklisted.",
  [-11]:
    "ERROR_INCOMPATIBLE: The add-on is incompatible (w.r.t. the compatibility range).",
  [-12]:
    "ERROR_UNSUPPORTED_ADDON_TYPE: The add-on type is not supported by the platform.",
};

async function installAddon(file) {
  let install = await lazy.AddonManager.getInstallForFile(file, null, {
    source: "internal",
  });

  if (install.error) {
    throw new lazy.error.UnknownError(ERRORS[install.error]);
  }

  return install.install().catch(() => {
    throw new lazy.error.UnknownError(ERRORS[install.error]);
  });
}

/** Installs addons by path and uninstalls by ID. */
export class Addon {
  /**
   * Install a Firefox addon.
   *
   * If the addon is restartless, it can be used right away.  Otherwise a
   * restart is required.
   *
   * Temporary addons will automatically be uninstalled on shutdown and
   * do not need to be signed, though they must be restartless.
   *
   * @param {string} path
   *     Full path to the extension package archive.
   * @param {boolean=} temporary
   *     True to install the addon temporarily, false (default) otherwise.
   *
   * @returns {Promise.<string>}
   *     Addon ID.
   *
   * @throws {UnknownError}
   *     If there is a problem installing the addon.
   */
  static async install(path, temporary = false) {
    let addon;
    let file;

    try {
      file = new lazy.FileUtils.File(path);
    } catch (e) {
      throw new lazy.error.UnknownError(`Expected absolute path: ${e}`, e);
    }

    if (!file.exists()) {
      throw new lazy.error.UnknownError(`No such file or directory: ${path}`);
    }

    try {
      if (temporary) {
        addon = await lazy.AddonManager.installTemporaryAddon(file);
      } else {
        addon = await installAddon(file);
      }
    } catch (e) {
      throw new lazy.error.UnknownError(
        `Could not install add-on: ${path}: ${e.message}`,
        e
      );
    }

    return addon.id;
  }

  /**
   * Uninstall a Firefox addon.
   *
   * If the addon is restartless it will be uninstalled right away.
   * Otherwise, Firefox must be restarted for the change to take effect.
   *
   * @param {string} id
   *     ID of the addon to uninstall.
   *
   * @returns {Promise}
   *
   * @throws {UnknownError}
   *     If there is a problem uninstalling the addon.
   */
  static async uninstall(id) {
    let candidate = await lazy.AddonManager.getAddonByID(id);
    if (candidate === null) {
      // `AddonManager.getAddonByID` never rejects but instead
      // returns `null` if the requested addon cannot be found.
      throw new lazy.error.UnknownError(`Addon ${id} is not installed`);
    }

    return new Promise(resolve => {
      let listener = {
        onOperationCancelled: addon => {
          if (addon.id === candidate.id) {
            lazy.AddonManager.removeAddonListener(listener);
            throw new lazy.error.UnknownError(
              `Uninstall of ${candidate.id} has been canceled`
            );
          }
        },

        onUninstalled: addon => {
          if (addon.id === candidate.id) {
            lazy.AddonManager.removeAddonListener(listener);
            resolve();
          }
        },
      };

      lazy.AddonManager.addAddonListener(listener);
      candidate.uninstall();
    });
  }
}