確保所有 API 的使用者啟用作業一致

Mustaq Ahmed
喬梅利
Joe Medley

為避免惡意指令碼濫用彈出式視窗、全螢幕等敏感 API,瀏覽器會透過使用者啟用來控管這些 API 的存取權。「使用者啟動」是指瀏覽工作階段狀態,與使用者動作相關:「有效」狀態通常是指使用者目前正在與網頁互動,或在網頁載入後完成互動。使用者手勢是同一個提案中常用但誤導性的字詞。舉例來說,使用者的滑動或撥動手勢並不會啟動網頁,因此從指令碼的角度來看,就不一定是使用者啟動作業。

現今的主要瀏覽器對於使用者啟動作業控制啟用限制的 API 的方式有很大的差異。在 Chrome 中,這項實作是以符記為基礎的模型,但過於複雜,無法為所有啟動管制的 API 定義一致的行為。舉例來說,Chrome 持續允許透過 postMessage()setTimeout() 呼叫存取啟用管制的 API。此外,PromiseXHR遊戲手把互動等也不支援使用者啟動程序。請注意,其中有些是長期存在的錯誤。

在第 72 版中,Chrome 會提供「使用者啟用」第 2 版,以便針對所有啟用管制的 API 完成使用者啟用作業。這可以解決上述的不一致問題 (還有許多問題,例如 MessageChannels),我們相信這有助於簡化使用者啟用的網頁開發工作。此外,新的實作項目針對建議的新規格提供了參考實作,希望長期下來將所有瀏覽器都整合在一起。

使用者啟用 v2 的運作方式為何?

新的 API 會在影格階層中的每個 window 物件,維持兩個使用者啟用狀態:過去使用者啟用狀態的固定位元 (如果影格曾看過使用者啟動作業),以及目前狀態的暫時性 (如果影格在約一秒後發現使用者啟動作業)。固定位元在設定後的生命週期內不會重設。暫時性位元會在每次使用者互動時設定,且會在到期間隔 (約一秒) 後或透過呼叫啟用 API (例如 window.open()) 而重設。

請注意,不同的啟用管制 API 仰賴不同的使用者啟用方式;新的 API 不會變更任何這些 API 特有的行為。舉例來說,每個使用者啟用作業只能有一個彈出式視窗,因為 window.open() 原本會取用使用者啟用作業,而如果影格 (或其任何子頁框) 看過使用者動作,Navigator.prototype.vibrate() 仍會繼續運作。

異動內容

  • 使用者啟用 v2 會正式呈現跨影格邊界的使用者啟用瀏覽權限:現在,與特定影格的互動,會啟用所有內含影格 (且僅限這些影格),無論其來源為何。(在 Chrome 72 版中,我們為提升所有相同來源影格的顯示設定,提供了暫時性的解決方法。一旦我們有辦法將使用者啟用作業明確傳遞給子頁框,我們就會移除這個解決方法)。
  • 如果從已啟用的影格呼叫啟用限制的 API,但從事件處理常式程式碼外呼叫,則只要使用者啟用狀態為「有效」(例如尚未過期或未使用) 即可。在使用者啟用 v2 之前,將會無條件失敗。
  • 到期時間間隔內,多項未使用的使用者互動都會納入與上次互動相對應的單一啟動項目。

啟用管制 API 不一致的範例

下方有兩個包含彈出式視窗的範例 (使用 window.open() 開啟),說明「使用者啟用」第 2 版會如何使啟用管制 API 的行為保持一致。

鏈結 setTimeout() 呼叫

這個範例來自 setTimeout() 示範。如果 click 處理常式嘗試在幾秒內嘗試開啟彈出式視窗,則無論程式碼「組成」延遲的方式為何,都應能順利執行。使用者啟用 v2 符合預期,因此下列每個事件處理常式都會在 click 中開啟彈出式視窗 (延遲時間為 100 毫秒):

function popupAfter100ms() {
  setTimeout(callWindowOpen, 100);
}

function asyncPopupAfter100ms() {
  setTimeout(popupAfter100ms, 0);
}

someButton.addEventListener('click', popupAfter100ms);
someButton.addEventListener('click', asyncPopupAfter100ms);

如果沒有使用者啟用 v2,第二個事件處理常式就無法在我們測試的所有瀏覽器中失敗。(即使是第一個在某些情況下會失敗)。

跨網域 postMessage() 呼叫

以下是 postMessage() 示範的範例。假設跨來源子頁框中的 click 處理常式會將兩則訊息直接傳送至上層頁框。上層頁框在收到下列任一訊息時應能開啟彈出式視窗 (但只能同時開啟兩者):

// Parent frame code
window.addEventListener('message', e => {
  if (e.data === 'open_popup' && e.origin === child_origin)
    window.open('about:blank');
});

// Child frame code:
someButton.addEventListener('click', () => {
  parent.postMessage('hi_there', parent_origin);
  parent.postMessage('open_popup', parent_origin);
});

如果沒有使用者啟用 v2,上層頁框無法在收到第二則訊息時開啟彈出式視窗。如果第一則訊息「鏈結」到其他跨來源影格,也會失敗 (也就是說,如果第一個接收方將訊息轉寄至另一個影格的話),

這項功能可與使用者啟用 v2 搭配運作,包括原始格式和鏈結。