Closed Bug 1220136 Opened 9 years ago Closed 8 years ago

WebExtensions support chrome.management.uninstallSelf

Categories

(WebExtensions :: Untriaged, defect, P3)

45 Branch
defect

Tracking

(firefox51 fixed)

RESOLVED FIXED
mozilla51
Iteration:
51.3 - Sep 19
Tracking Status
firefox51 --- fixed

People

(Reporter: markus.hartung, Assigned: bsilverberg, Mentored)

References

Details

(Keywords: dev-doc-complete, Whiteboard: [management][berlin]triaged)

Attachments

(2 files, 1 obsolete file)

User Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36
Component: Untriaged → WebExtensions
Product: Firefox → Toolkit
Status: UNCONFIRMED → NEW
Ever confirmed: true
Whiteboard: [management][berlin]
Whiteboard: [management][berlin] → [management][berlin][good first bug]
Andrew, can you add some details about how this should be implemented?

Thanks!
Mentor: aswan
The short answer is to find the addon with AddonManager.getAddonByID [1] and then call its uninstall method [2].  The twist is that during uninstall, the addon will be shut down which will tear down the context in which the code that calls this function is running, so some care will need to be taken to do that work from outside of the addon.  Which begs the question, why does this method have a callback?  I suppose it could be used to report an error that happens asynchronously after the original call but before the addon has been disabled?  Weird...

[1] https://developer.mozilla.org/en-US/Add-ons/Add-on_Manager/AddonManager#getAddonByID()
[2] https://developer.mozilla.org/en-US/Add-ons/Add-on_Manager/Addon#uninstall()
(In reply to Andrew Swan [:aswan] from comment #2)
> The short answer is to find the addon with AddonManager.getAddonByID [1] and
> then call its uninstall method [2].

I think AddonManager.getAddonByInstanceID is preferred, now.

> Which begs the question, why does this method have a callback?  I suppose it
> could be used to report an error that happens asynchronously after the
> original call but before the addon has been disabled?

Yeah, that would be my guess too. It would be nice to know if/when Chrome
calls it when there are no errors.
Priority: -- → P3
Summary: WebExtensions support chrome.managment.uninstallSelf → WebExtensions support chrome.management.uninstallSelf
Whiteboard: [management][berlin][good first bug] → [management][berlin][good first bug]triaged
Blocks: 1282979
No longer blocks: webext-port-from-doc-to-pdf
No longer blocks: webext-port-avg-web-tuneup
Keywords: good-first-bug
Whiteboard: [management][berlin][good first bug]triaged → [management][berlin]triaged
Assignee: nobody → bob.silverberg
Status: NEW → ASSIGNED
Andrew and I met to discuss this API and I sent out an "Intent to Implement" note with a question about how to address the `showConfirmDialog` option and behaviour. I will update the bug with the results of the ensuing discussion.
Chrome supports an argument to this method which is an `options` object which can contain a single boolean property: `showConfirmDialog`. It is optional and defaults to `false`. If a developer specifies `showConfirmDialog: true` when calling management.uninstallSelf then a modal dialog is displayed to the user asking if they want to uninstall the extension. The dialog (a screenshot of which is attached to this bug), simply contains the message 'Remove "{extension name}"?' with a `Cancel`
and a `Remove` button. There is also an icon to the left of the message which looks like the Chrome extensions puzzle piece.

In the Chrome implementation, a developer has no control over what is displayed in this dialog - there is no opportunity to provide any additional information, or customize it in any way. This does not seem like a very good user experience and we think our implementation could be better.

Requesting UX help to determine how best to prompt a user for whether they would like to uninstall the extension or not.
Flags: needinfo?(mjaritz)
Keywords: good-first-bug
The modal dialog that is displayed when uninstallSelf is called in a Chrome extension with showConfirmDialog: true.
In what scenarios and how often is this used, with and without dialog?

(In reply to Bob Silverberg [:bsilverberg] from comment #7)
> In the Chrome implementation, a developer has no control over what is
> displayed in this dialog - there is no opportunity to provide any additional
> information, or customize it in any way. This does not seem like a very good
> user experience and we think our implementation could be better.

If extensions have the ability to uninstall themselves without a dialog, I wonder what reason they might have to use that dialog? As this is initiated by the extensions, they themselves have the ability to show any messaging they want prior to calling this method. Having this dialog seams to be the easier way for developers that do not want to bother with building their own message. - And I guess in there lies the ability of improvement for us to provide a better default for devs that do not want to build their own messaging.

> Requesting UX help to determine how best to prompt a user for whether they
> would like to uninstall the extension or not.

From what I see so far the improvement we could gain with working on this seams rather small.
We could offer a way for devs to add to the message, like we will for the install message (https://github.com/mozilla/addons/issues/112#issuecomment-223350744) but it seams they have plenty of ways to talk to their users prior to calling this method. And having it as a modal dialog seams OK as well as this message requires the user to take action.

If I misunderstood the context for this message please clarify, if not, I think it is good as is for now.
Flags: needinfo?(mjaritz)
(In reply to Markus Jaritz [:maritz] (UX) from comment #9)

Thanks Markus. 

> In what scenarios and how often is this used, with and without dialog?
> 

We don't really have that information right now. I want to do some digging through code for Chrome extensions to see if I can find some more that use it. The only use I found thus far does not provide the dialog to a user.

> (In reply to Bob Silverberg [:bsilverberg] from comment #7)
> 
> If extensions have the ability to uninstall themselves without a dialog, I
> wonder what reason they might have to use that dialog? As this is initiated
> by the extensions, they themselves have the ability to show any messaging
> they want prior to calling this method. 

That is a good point. Even if the uninstall isn't happening as a response to a user action (such as clicking a button), the add-on developer still has total control of the code that surrounds the call to uninstallSelf and therefore could implement their own prompt if they wish.

> Having this dialog seams to be the
> easier way for developers that do not want to bother with building their own
> message. - And I guess in there lies the ability of improvement for us to
> provide a better default for devs that do not want to build their own
> messaging.
> 
> > Requesting UX help to determine how best to prompt a user for whether they
> > would like to uninstall the extension or not.
> 
> From what I see so far the improvement we could gain with working on this
> seams rather small.
> We could offer a way for devs to add to the message, like we will for the
> install message
> (https://github.com/mozilla/addons/issues/112#issuecomment-223350744) but it
> seams they have plenty of ways to talk to their users prior to calling this
> method. And having it as a modal dialog seams OK as well as this message
> requires the user to take action.
> 
> If I misunderstood the context for this message please clarify, if not, I
> think it is good as is for now.

To summarize what you are suggesting: We should just implement the default prompt as a modal dialog much like Chrome does, and if a developer wants a better experience for their users they should implement their own prompting around the call to uninstallSelf. Is that correct?
(In reply to Bob Silverberg [:bsilverberg] from comment #10)
> To summarize what you are suggesting: We should just implement the default
> prompt as a modal dialog much like Chrome does, and if a developer wants a
> better experience for their users they should implement their own prompting
> around the call to uninstallSelf. Is that correct?

Yes, that is correct. Thanks for the summary.
Comment on attachment 8784360 [details]
Bug 1220136 - WebExtensions support chrome.management.uninstallSelf,

https://reviewboard.mozilla.org/r/73830/#review71684

::: toolkit/components/extensions/ext-management.js:64
(Diff revision 1)
>            }
>          }));
>        },
> +      uninstallSelf: function(options) {
> +        if (options && options.showConfirmDialog) {
> +          let message = options.dialogMessage ||

Ideally we would localize the message and title, but I'm not sure how to do that. Do we have any existing examples in WebExtensions that localize text?

::: toolkit/mozapps/extensions/test/xpcshell/test_ext_management.js:139
(Diff revision 1)
>    equal(extInfo.id, id, "getSelf returned the expected id");
>    equal(extInfo.installType, "normal", "getSelf returned the expected installType");
>    yield extension.unload();
>  });
> +
> +add_task(function* test_management_uninstall_no_prompt() {

It would be nice to have automated tests that use the prompt as well, both a test where the response is "Yes" and one where it is "No". I tried getting this to work with a plain mochitest and a chrome mochitest but couldn't seem to get it working. Is this possible, to interact with the prompt in a test, and if so do you know of any examples I could look at?

It would be nice if we could simply mock the PromptService and tell it to return `true` or `false` during the test, as the case may be, but I'm not sure how to do that either.
Comment on attachment 8784360 [details]
Bug 1220136 - WebExtensions support chrome.management.uninstallSelf,

https://reviewboard.mozilla.org/r/73832/#review72060

How about tests of prompts and of error cases?

::: toolkit/components/extensions/ext-management.js:5
(Diff revision 1)
>  /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
>  /* vim: set sts=2 sw=2 et tw=80: */
>  "use strict";
>  
> +var {classes: Cc, interfaces: Ci} = Components;

make this const

::: toolkit/components/extensions/ext-management.js:65
(Diff revision 1)
>          }));
>        },
> +      uninstallSelf: function(options) {
> +        if (options && options.showConfirmDialog) {
> +          let message = options.dialogMessage ||
> +                        `Are you sure you want to uninstall the "${extension.name}" extension?`;

Putting on my amateur UX hat, I think its weird to phrase a question "are you sure you want to " for something the user didn't actually initiate.  I think this would be clearer as something like "Extension ${name} has asked to be removed.  Is this okay?"

::: toolkit/components/extensions/ext-management.js:76
(Diff revision 1)
> +          }
> +        }
> +        return new Promise((resolve, reject) => AddonManager.getAddonByID(extension.id, addon => {
> +          try {
> +            addon.uninstall();
> +            resolve();

Doesn't this generate the "promise resolved after context unloaded" error?

::: toolkit/components/extensions/ext-management.js:78
(Diff revision 1)
> +        return new Promise((resolve, reject) => AddonManager.getAddonByID(extension.id, addon => {
> +          try {
> +            addon.uninstall();
> +            resolve();
> +          } catch (e) {
> +            reject(e);

This will show up to the caller as the old "unexpected error".  To propagate the message you want `reject({message: e.message})`.
Can you give the variable a longer name (e.g., err) while you're at it?

::: toolkit/mozapps/extensions/test/xpcshell/test_ext_management.js:179
(Diff revision 1)
> +    };
> +    AddonManager.addAddonListener(listener);
> +  });
> +
> +  yield extension.startup();
> +  yield extension.markUnloaded();

I think this should come after the uninstall has happened.
Attachment #8784360 - Flags: review?(aswan)
Comment on attachment 8784360 [details]
Bug 1220136 - WebExtensions support chrome.management.uninstallSelf,

https://reviewboard.mozilla.org/r/73832/#review72060

> Putting on my amateur UX hat, I think its weird to phrase a question "are you sure you want to " for something the user didn't actually initiate.  I think this would be clearer as something like "Extension ${name} has asked to be removed.  Is this okay?"

We should probably always include a stock message, even if the extension includes its own.

I agree with Andrew about the text, though. My suggestion would be to use `confirmEx` rather than `confirm`, with a message like `The extension "<name>" is requesting to be uninstalled. What would you like to do?`, with the buttons "Uninstall" and "Keep Installed".


Also, user-visible messages need to be localized. We can get away with English-only copy for errors that we only show to developers, but the main UI needs to be fully localized.
Comment on attachment 8784360 [details]
Bug 1220136 - WebExtensions support chrome.management.uninstallSelf,

https://reviewboard.mozilla.org/r/73832/#review72070

::: toolkit/components/extensions/ext-management.js:67
(Diff revision 1)
> +          let promptService = Cc["@mozilla.org/embedcomp/prompt-service;1"].
> +                              getService(Ci.nsIPromptService);

Please use a lazy service getter for this. That aside, the '.' should always be on the same line as the method/property name.

::: toolkit/components/extensions/ext-management.js:70
(Diff revision 1)
> +                        `Are you sure you want to uninstall the "${extension.name}" extension?`;
> +          let title = `Uninstall ${extension.name}`;
> +          let promptService = Cc["@mozilla.org/embedcomp/prompt-service;1"].
> +                              getService(Ci.nsIPromptService);
> +          if (!promptService.confirm(null, title, message)) {
> +            return Promise.reject({message: `User cancelled uninstall of extension ${extension.id}`});

Please use a static string for the error name. The caller knows what extension it's trying to uninstall.

::: toolkit/mozapps/extensions/test/xpcshell/test_ext_management.js:139
(Diff revision 1)
>    equal(extInfo.id, id, "getSelf returned the expected id");
>    equal(extInfo.installType, "normal", "getSelf returned the expected installType");
>    yield extension.unload();
>  });
> +
> +add_task(function* test_management_uninstall_no_prompt() {

This test can (and probably should) go in `toolkit/components/extensions`.

::: toolkit/mozapps/extensions/test/xpcshell/test_ext_management.js:181
(Diff revision 1)
> +  });
> +
> +  yield extension.startup();
> +  yield extension.markUnloaded();
> +  let addon = yield promiseAddonByID(id);
> +  notEqual(addon, null);

Please include a message in all assertions. It's pretty hard to make sense of test output without them.

We should probably add an ESLint rule for that...
Comment on attachment 8784360 [details]
Bug 1220136 - WebExtensions support chrome.management.uninstallSelf,

https://reviewboard.mozilla.org/r/73832/#review72060

> Doesn't this generate the "promise resolved after context unloaded" error?

Yeah, that was left over from an older version. I have removed that, as it should never fire, but I am still getting "Promise resolved after context unloaded" when an uninstall is allowed to complete. I wonder if this is because the API method is marked async and so the framework is generating a Promise for it? Any ideas about how I can eliminate that issue?
Comment on attachment 8784360 [details]
Bug 1220136 - WebExtensions support chrome.management.uninstallSelf,

https://reviewboard.mozilla.org/r/73832/#review72912

This is close. Mainly just some nits.

::: toolkit/components/extensions/ext-management.js:5
(Diff revision 3)
>  /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
>  /* vim: set sts=2 sw=2 et tw=80: */
>  "use strict";
>  
> +const {classes: Cc, interfaces: Ci} = Components;

Please also define `Cu` and `Cr` so they're there when someone tries to use them.

::: toolkit/components/extensions/ext-management.js:8
(Diff revision 3)
>  "use strict";
>  
> +const {classes: Cc, interfaces: Ci} = Components;
> +
> +XPCOMUtils.defineLazyGetter(this, "strBundle", function() {
> +  const bunService = Cc["@mozilla.org/intl/stringbundle;1"].getService(Ci.nsIStringBundleService);

Hm. Can we please call this anything other than `bunService`? How about `stringSvc`?

::: toolkit/components/extensions/ext-management.js:17
(Diff revision 3)
>                                    "resource://gre/modules/AddonManager.jsm");
> +XPCOMUtils.defineLazyServiceGetter(this, "promptService",
> +                                   "@mozilla.org/embedcomp/prompt-service;1",
> +                                   "nsIPromptService");
> +
> +function getLocalizedString(key, formatArgs) {

Please just use `...args` rather than passing an array. And we probably may as well just call this `_`.

::: toolkit/components/extensions/ext-management.js:72
(Diff revision 3)
>                });
>              }
>  
>              resolve(extInfo);
> -          } catch (e) {
> -            reject(e);
> +          } catch (err) {
> +            reject({message: err.message});

This exposes the message from an unexpected internal error to the caller, which we don't want. Please reject with the actual error so that it gets reported, and an unexpected error gets reported to the caller.

::: toolkit/components/extensions/ext-management.js:76
(Diff revision 3)
> -          } catch (e) {
> -            reject(e);
> +          } catch (err) {
> +            reject({message: err.message});
>            }
>          }));
>        },
> +      uninstallSelf: function(options) {

Please add empty line.

::: toolkit/components/extensions/ext-management.js:77
(Diff revision 3)
> -            reject(e);
> +            reject({message: err.message});
>            }
>          }));
>        },
> +      uninstallSelf: function(options) {
> +        if (options && options.showConfirmDialog) {

Please wrap the whole function in `new Promise(...)` rather than using a try-catch.

::: toolkit/components/extensions/ext-management.js:80
(Diff revision 3)
>        },
> +      uninstallSelf: function(options) {
> +        if (options && options.showConfirmDialog) {
> +          let message = getLocalizedString("uninstallDialogMessage", [extension.name]);
> +          if (options.dialogMessage) {
> +            message = options.dialogMessage + "\n" + message;

Please use template strings.

::: toolkit/components/extensions/ext-management.js:93
(Diff revision 3)
> +          AddonManager.getAddonByID(extension.id, addon => {
> +            addon.uninstall();
> +          });

You're not returning a promise in this case, which means that this will resolve immediately.

::: toolkit/components/extensions/ext-management.js:97
(Diff revision 3)
> +        try {
> +          AddonManager.getAddonByID(extension.id, addon => {
> +            addon.uninstall();
> +          });
> +        } catch (err) {
> +          return Promise.reject({message: err.message});

Same issue as above.

::: toolkit/components/extensions/test/xpcshell/test_ext_management_uninstall_self.js:5
(Diff revision 3)
> +XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
> +                                  "resource://gre/modules/AddonManager.jsm");
> +XPCOMUtils.defineLazyModuleGetter(this, "AddonTestUtils",
> +                                  "resource://testing-common/AddonTestUtils.jsm");
> +XPCOMUtils.defineLazyModuleGetter(this, "MockRegistrar",
> +                                  "resource://testing-common/MockRegistrar.jsm");

These are used immediately. They don't need to be lazy imports.

::: toolkit/components/extensions/test/xpcshell/test_ext_management_uninstall_self.js:58
(Diff revision 3)
> +    });
> +  }
> +
> +  let extension = ExtensionTestUtils.loadExtension({
> +    manifest,
> +    background: background,

background,

::: toolkit/components/extensions/test/xpcshell/test_ext_management_uninstall_self.js:94
(Diff revision 3)
> +  yield waitForUninstalled;
> +  yield extension.markUnloaded();
> +});
> +
> +add_task(function* test_management_uninstall_prompt_keep() {
> +  promptService.confirmEx = () => {return 1};

Can we also do something to test the strings we pass here? Nothing to complicated, just some basic sanity checks.

::: toolkit/locales/en-US/chrome/global/extensions.properties:23
(Diff revision 3)
>  
>  #LOCALIZATION NOTE (csp.error.illegal-host-wildcard) %2$S a protocol name, such as "http", which appears as "http:", as it would in a URL.
>  csp.error.illegal-host-wildcard = %2$S: wildcard sources in ‘%1$S’ directives must include at least one non-generic sub-domain (e.g., *.example.com rather than *.com)
> +
> +#LOCALIZATION NOTE (uninstallDialogTitle) %S is the name of the extension which is about to be uninstalled.
> +uninstallDialogTitle = Uninstall %S

Sorry, I shouldn't have said it doesn't matter. I don't care much about camel case vs. hyphens, but the message names should still be hierarchical.

    uninstall.confirmation.title
    uninstall.confirmation.button-1.label
    ...

::: toolkit/locales/en-US/chrome/global/extensions.properties:26
(Diff revision 3)
> +
> +#LOCALIZATION NOTE (uninstallDialogTitle) %S is the name of the extension which is about to be uninstalled.
> +uninstallDialogTitle = Uninstall %S
> +
> +#LOCALIZATION NOTE (uninstallDialogMessage) %S is the name of the extension which is about to be uninstalled.
> +uninstallDialogMessage = The extension "%S" is requesting to be uninstalled. What would you like to do?

Ascii quotes are no longer allowed in localization files. Please use `“%S”` instead.
Attachment #8784360 - Flags: review?(kmaglione+bmo)
Comment on attachment 8784360 [details]
Bug 1220136 - WebExtensions support chrome.management.uninstallSelf,

https://reviewboard.mozilla.org/r/73832/#review72912

> Please wrap the whole function in `new Promise(...)` rather than using a try-catch.

I'm not sure I understand the suggestion, but I tried something, which I'm guessing isn't correct. If it is not, could you be a bit more specific about what you'd like to see.

> You're not returning a promise in this case, which means that this will resolve immediately.

That was intentional. I know it's a bit weird, and there's probably a better way, but the issue is that the caller can never wait for the promise to resolve becuase if we uninstall then the caller no longer exists. Executing this immediately seems like the only practical thing to do.
Comment on attachment 8784360 [details]
Bug 1220136 - WebExtensions support chrome.management.uninstallSelf,

https://reviewboard.mozilla.org/r/73832/#review73310

::: toolkit/components/extensions/ext-management.js:95
(Diff revisions 3 - 4)
> -            return Promise.reject({message: "User cancelled uninstall of extension"});
> +              return reject({message: "User cancelled uninstall of extension"});
> -          }
> +            }
> -        }
> +          }
> -        try {
>            AddonManager.getAddonByID(extension.id, addon => {
>              addon.uninstall();

We should probably put a try-catch around the `uninstall` call, and reject if there's an error. Some add-ons can't be uninstalled.

I suppose it might make more sense to check `PERM_CAN_UNINSTALL` first.
Attachment #8784360 - Flags: review?(kmaglione+bmo) → review+
Comment on attachment 8784360 [details]
Bug 1220136 - WebExtensions support chrome.management.uninstallSelf,

https://reviewboard.mozilla.org/r/73832/#review72912

> That was intentional. I know it's a bit weird, and there's probably a better way, but the issue is that the caller can never wait for the promise to resolve becuase if we uninstall then the caller no longer exists. Executing this immediately seems like the only practical thing to do.

Even attempting to resolve immediately isn't guaranteed to work. I'd rather not resolve at all than try to race the uninstall code.
Comment on attachment 8784360 [details]
Bug 1220136 - WebExtensions support chrome.management.uninstallSelf,

https://reviewboard.mozilla.org/r/73832/#review72912

> Even attempting to resolve immediately isn't guaranteed to work. I'd rather not resolve at all than try to race the uninstall code.

Sorry, I'm still not sure what exactly I need to do differently here. I tried adding a `return resolve()` after the call to `addon.uninstall()` (which I have also now wrapped in a `try`) and it works fine, but it generates that "Promise resolved after context unloaded" error in the console. If we don't care about that error showing up in the console then I guess that's the thing to do, or is there something else I can do?
Try looks good, requesting checkin.
Iteration: --- → 51.3 - Sep 12
Keywords: checkin-needed
Pushed by ryanvm@gmail.com:
https://hg.mozilla.org/integration/autoland/rev/73e11489018d
WebExtensions support chrome.management.uninstallSelf, r=kmag
Keywords: checkin-needed
Try would have looked better to me with some Windows tests (if you do win64, as in Win8, you have to explicitly check the boxes for tests and get an awkward "try: -b do -p linux64,macosx64,win64 -u xpcshell[x64,10.10,Windows 8],mochitests[x64,10.10,Windows 8] -t none[x64,10.10,Windows 8]", while win32 gives you Win7 tests without having to beg for them).

Backed out in https://hg.mozilla.org/integration/autoland/rev/4474f63eaf76 for https://treeherder.mozilla.org/logviewer.html#?job_id=3021585&repo=autoland (on all three versions of Windows).
Attached patch Patch to use for re-landing (obsolete) — Splinter Review
I believe I fixed the problem, but MozReview will not allow me to update the review, so I am attaching a patch separately.
Attachment #8784360 - Attachment is obsolete: true
Comment on attachment 8788543 [details] [diff] [review]
Patch to use for re-landing

This is no longer needed as I re-opened the MozReview and pushed the new commit there.
Attachment #8788543 - Attachment is obsolete: true
Finally passing on try on Windows!
Keywords: checkin-needed
https://hg.mozilla.org/mozilla-central/rev/14a2335ceea6
Status: ASSIGNED → RESOLVED
Closed: 8 years ago
Resolution: --- → FIXED
Target Milestone: --- → mozilla51
Keywords: dev-doc-needed
I've written some docs for the management API:
https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/management

including uninstallSelf():
https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/management/uninstallSelf

Please let me know if this looks OK.
Flags: needinfo?(bob.silverberg)
Thanks Will. The docs for management.uninstallSelf look good, but are missing one thing. We enhanced the functionality in Firefox to accept an extra properties in the options object that can be passed to uninstallSelf.

A developer can pass a `dialogMessage` property in addition to the `showConfirmDialog` property and when the dialog is displayed it will display the message specified in `dialogMessage` above the standard message.

I'm not sure if that deserves an example to illustrate, but it should at least be documented.
Flags: needinfo?(bob.silverberg)
Thanks Bob. I've documented "dialogMessage".
Thanks Will!
Product: Toolkit → WebExtensions
You need to log in before you can comment on or make changes to this bug.