Firefox WebExtension:如何 运行 在 Disable/Uninstall 之前编写代码?

Firefox WebExtension: How Do I Run Code Prior to Disable/Uninstall?

我最近将我的一个 GreaseMonkey 脚本转换为一个 WebExtension,只是为了获得对这个过程的第一印象。现在我已经到了一个地步,当所述扩展名是 disabled/uninstalled.

时,最好进行一些清理或简单地撤消我的所有更改

根据我在 Mozilla 页面上看到的内容,runtime.onSuspend 应该可以解决问题。不幸的是,它看起来还没有实现(我在常规的 Firefox 发布频道上)。

换句话说,我想做的是 运行 代码作为用户 removing/disabling 我的扩展的结果,这样我就可以清理监听器等,并且通常将选项卡恢复到他们的现状,我。即,撤消扩展所做的所有更改。

你最初的措辞有点不清楚你到底想要什么。因此,此答案还包含有关在某些情况下您可以接收卸载通知的一种方式的信息。

运行 WebExtension 附加组件中 uninstall/disable 之前的代码:
不,即使支持,runtime.onSuspend 事件也不会执行您想要的操作。它用于向事件页面发出即将被卸载的信号。当页面正在侦听的事件处理完成时,甚至页面也会定期卸载。这并不表示正在卸载扩展程序。

"Determine" 你的 "WebExtension was disabled/uninstalled":
如果您的问题确实是您在问题的最后一行中陈述的:“...有没有办法确定 WebExtension 是否为 disabled/uninstalled?”然后,看起来您可以使用从 Firefox 47 开始实现的 runtime.setUninstallURL()。这将允许您设置一个 URL 以在卸载附加组件时访问。这可以在您的服务器上使用,以注意附加组件已卸载。它不会通知您的 WebExtension 它已被卸载,也不会在发生这种情况时允许您 运行 在 WebExtension 中编写代码。

不幸的是,您无法在您的 WebExtension 中使用检测来访问此 URL,这表明您的 WebExtension 正在 uninstalled/disabled。根据测试,此 URL 在 WebExtension 完全卸载后 被访问 。另外,禁用WebExtension时不访问,禁用后卸载也不访问。只有在启用附加组件的情况下卸载 WebExtension 时才会访问它。事实上,这是一个 JavaScript 调用,当启用扩展时只有 运行 ,人们会期望页面只会在离开启用状态时打开。

通过将以下行添加到 WebExtension 并查看页面何时打开来完成测试:

chrome.runtime.setUninstallURL("http://www.google.com");

考虑到它的实际功能(仅在启用 WebExtension 并直接卸载时访问),将其用作 "a way to determine whether a WebExtension was disabled/uninstalled" 将仅部分有效。应该清楚的是,如果加载项在卸载前被禁用,您将不会收到访问此 URL 的通知。

不正确。第一部分(关于 onSuspend 事件)实际上是不正确的。关于 setUninstallURL 的部分是相关的,但不回答问题,因为它不允许您将选项卡恢复到其原始状态(如您在问题中所问)。

在这个回答中,我将首先澄清关于 runtime.onSuspend 的误解,然后解释如何在禁用扩展时 运行 为内容脚本编写代码。

关于runtime.onSuspend

chrome.runtime.onSuspend and chrome.runtime.onSuspendCanceled events have nothing to do with a disabled/uninstalled extension. The events are defined for event pages,它们基本上是在一段时间不活动后暂停(卸载)的后台页面。当事件页面因挂起即将卸载时,调用runtime.onSuspend。如果在该事件期间调用分机API(例如发送分机消息),暂停将被取消并触发onSuspendCanceled事件。

当由于浏览器关闭或卸载而卸载扩展程序时,无法延长扩展程序的生命周期。因此,您不能依赖这些事件来执行 运行 异步任务(例如清理后台页面中的选项卡)。

此外,这些事件在内容脚本中不可用(仅扩展页面,如背景页面),因此这些不能用于同步清理内容脚本逻辑。

从上面可以明显看出,runtime.onSuspend 与禁用后清理的目标无关。 Chrome 更不用说 Firefox(Firefox 不支持事件页面,这些事件将毫无意义)。

运行扩展 tabs/content 脚本中的宁代码 disable/uninstall

Chrome 扩展中的一个常见模式是使用 port.onDisconnect event to detect that the background page has unloaded, and use that to infer that the extension might have unloaded (combined with option 1 of this method 以获得更高的准确性)。 Chrome 的内容脚本在禁用扩展后仍然存在,因此可用于 运行 异步清理代码。
这在 Firefox 中是不可能的,因为当 Firefox 扩展被禁用时,内容脚本的执行上下文在 port.onDisconnect 事件有机会触发之前被破坏(至少,直到 bugzil.la/1223425 被修复) .

尽管存在这些限制,但仍然可以 运行 在禁用加载项时清理内容脚本的逻辑。此方法基于以下事实:在 Firefox 中,禁用附加组件时会删除插入 tabs.insertCSS 的样式 sheet。
我将讨论两种利用此特性的方法。第一种方法允许执行任意代码。第二种方法不提供任意代码的执行,但如果你只是想隐藏一些扩展插入的DOM元素,它更简单和足够。

方法一:运行禁用扩展时页面中的代码

观察样式变化的方法之一是声明 CSS transitions and using transition events to detect CSS property changes。 为此,您需要以仅影响 HTML 元素的方式构建样式 sheet。因此,您需要生成一个唯一的选择器(class 名称、ID、...)并将其用于您的 HTML 元素和样式 sheet.

这是您必须放入后台脚本的代码:

chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
    if (message !== 'getStyleCanary') return;

    // Generate a random class name, insert a style sheet and send
    // the class back to the caller if successful.
    var CANARY_CLASS = '_' + crypto.getRandomValues(new Uint32Array(2)).join('');
    var code = '.' + CANARY_CLASS + ' { opacity: 0 !important; }';
    chrome.tabs.insertCSS(sender.tab.id, {
        code,
        frameId: sender.frameId,
        runAt: 'document_start',
    }, function() {
        if (chrome.runtime.lastError) {
            // Failed to inject. Frame unloaded?
            sendResponse();
        } else {
            sendResponse(CANARY_CLASS);
        }
    });
    return true; // We will asynchronously call sendResponse.
});

在内容脚本中:

chrome.runtime.sendMessage('getStyleCanary', function(CANARY_CLASS) {
    if (!CANARY_CLASS) {
        // Background was unable to insert a style sheet.
        // NOTE: Consider retry sending the message in case
        // the background page was not ready yet.
        return;
    }

    var s = document.createElement('script');
    s.src = chrome.runtime.getURL('canaryscript.js');
    s.onload = s.remove;
    s.dataset.canaryClass = CANARY_CLASS;

    // This function will become available to the page and be used
    // by canaryscript.js. NOTE: exportFunction is Firefox-only.
    exportFunction(function() {}, s, {defineAs: 'checkCanary'}); 

    (document.body || document.documentElement).appendChild(s);
});

我在上面使用了脚本标签,因为这是 运行 页面中的脚本而不会被页面的内容安全策略阻止的唯一方法。确保将 canaryscript.js 添加到 web_accessible_resources in manifest.json,否则脚本将不会加载。

如果 运行 清理代码不是关键(例如,因为您还使用我稍后解释的方法 2),那么您最好使用内联脚本而不是外部脚本(即使用 s.textContent = '<content of canaryscript.js>' 而不是 s.src = ...)。这是因为将 .src 与扩展资源一起使用会引入 fingerprinting vulnerability to Firefox (bug 1372288).

这是canaryscript.js的内容:

(function() {
    // Thes two properties are set in the content script.
    var checkCanary = document.currentScript.checkCanary;
    var CANARY_CLASS = document.currentScript.dataset.canaryClass;

    var canary = document.createElement('span');
    canary.className = CANARY_CLASS;
    // The inserted style sheet has opacity:0. Upon removal a transition occurs.
    canary.style.opacity = '1';
    canary.style.transitionProperty = 'opacity';
    // Wait a short while to make sure that the content script destruction
    // finishes before the style sheet is removed.
    canary.style.transitionDelay = '100ms';
    canary.style.transitionDuration = '1ms';
    canary.addEventListener('transitionstart', function() {
       // To avoid inadvertently running clean-up logic when the event
       // is triggered by other means, check whether the content script
       // was really destroyed.
       try {
            // checkCanary will throw if the content script was destroyed.
            checkCanary();
            // If we got here, the content script is still valid.
            return;
        } catch (e) {
        }
        canary.remove();

        // TODO: Put the rest of your clean up code here.
    });
    (document.body || document.documentElement).appendChild(canary);
})();

注意:CSS 转换事件仅在选项卡处于活动状态时触发。如果选项卡处于非活动状态,则在显示选项卡之前不会触发转换事件。

注意:exportFunction 是 Firefox 专用的扩展方法,用于在不同的执行上下文中定义函数(在上面的示例中,该函数是在页面的上下文中定义的,可用于脚本 运行在该页面中)。

所有其他 APIs 在其他浏览器中也可用 (Chrome/Opera/Edge),但代码不能用于检测禁用的扩展,因为样式 sheets 来自 tabs.insertCSS 不会在卸载时被删除(我只测试了 Chrome;它可能在 Edge 中有效)。

方法二:卸载后目视还原

方法 1 允许您 运行 任意代码,例如删除您在页面中插入的所有元素。作为从 DOM 中删除元素的替代方法,您还可以选择通过 CSS.
隐藏元素 下面我展示了如何修改方法 1 以隐藏元素而无需 运行 其他代码(例如 canaryscript.js)。

当您的内容脚本在 DOM 中创建要插入的元素时,您可以使用内联样式隐藏它:

var someUI = document.createElement('div');
someUI.style.display = 'none'; // <-- Hidden
// CANARY_CLASS is the random class (prefix) from the background page.
someUI.classList.add(CANARY_CLASS + 'block');
// ... other custom logic, and add to document.

在使用 tabs.insertCSS 添加的样式 sheet 中,然后使用 !important 标志定义所需的 display 值,以便覆盖内联样式:

// Put this snippet after "var code = '.' + CANARY_CLASS, above.
code += '.' + CANARY_CLASS + 'block {display: block !important;}';

以上示例是有意通用的。如果您有多个具有不同 CSS display 值的 UI 元素(例如 blockinline、...),那么您可以添加多行这些行重新使用我提供的框架。

为了显示方法 2 优于方法 1 的简单性:您可以使用相同的后台脚本(进行上述修改),并在内容脚本中使用以下内容:

// Example: Some UI in the content script that you want to clean up.
var someUI = document.createElement('div');
someUI.textContent = 'Example: This is a test';
document.body.appendChild(someUI);

// Clean-up is optional and a best-effort attempt.
chrome.runtime.sendMessage('getStyleCanary', function(CANARY_CLASS) {
    if (!CANARY_CLASS) {
        // Background was unable to insert a style sheet.
        // Do not add clean-up classes.
        return;
    }
    someUI.classList.add(CANARY_CLASS + 'block');
    someUI.style.display = 'none';
});

如果您的扩展有多个元素,请考虑将 CANARY_CLASS 的值缓存在一个局部变量中,以便您只在每个执行上下文中插入一个新样式 sheet。