API 間で一貫したユーザー アクティベーションを実現

Mustaq Ahmed
Joe Medley
Joe Medley

悪意のあるスクリプトがポップアップや全画面表示などの機密性の高い API を悪用することを防ぐため、ブラウザはユーザーのアクティブ化を通じてこれらの API へのアクセスを制御します。ユーザー アクティベーションは、ユーザー アクションに関するブラウジング セッションの状態です。「アクティブ」状態は通常、ユーザーが現在ページを操作しているか、ページの読み込み後に操作を完了していることを意味します。「ユーザー ジェスチャー」は、同じ考え方に対してよく使われている用語ですが、誤解を招くおそれもあります。たとえば、ユーザーがスワイプやフリック操作を行ってもページはアクティブにならず、スクリプトの観点からはユーザーのアクティブ化にはなりません。

現在の主要なブラウザでは、ユーザー アクティベーションがアクティベーション制限型 API を制御する方法が大きく異なります。Chrome での実装はトークンベースのモデルに基づいていましたが、有効化制御型 API 全体で一貫した動作を定義するには、複雑すぎることが判明しました。たとえば、Chrome では、postMessage()setTimeout() 呼び出しによるアクティベーション依存型 API への不完全なアクセスが許可されています。ユーザー アクティベーションは、PromiseXHRゲームパッド操作などでサポートされていませんでした。なお、この中にはよく知られているバグもあります。

バージョン 72 の Chrome には User Activation v2 が搭載されています。これにより、有効化の制限があるすべての API でユーザー アクティベーションが実現します。これにより、前述の不整合(および MessageChannels などの不整合)が解消され、ユーザーのアクティベーションに関するウェブ開発が容易になると考えられます。さらに、新しい実装では、すべてのブラウザを長期的に統合することを目的とした新しい仕様案のリファレンス実装を提供します。

ユーザー アクティベーション v2 の仕組み

新しい API は、フレーム階層内のすべての window オブジェクトで 2 ビットのユーザー アクティベーション状態を維持します。つまり、過去のユーザー アクティベーション状態(フレームでユーザーのアクティベーションを確認した場合)用のスティッキー ビットと、現在の状態の一時的なビット(フレームが約 1 秒後にフレームでユーザーのアクティベーションを検出した場合)です。スティッキー ビットは、設定されたフレームの存続期間中にリセットされません。一時的なビットはすべてのユーザー操作で設定され、有効期限(約 1 秒)が経過した後、またはアクティベーションを使用する API(window.open() など)の呼び出しによってリセットされます。

有効化制御型 API によって、ユーザー アクティベーションに依存する方法が異なります。新しい API では、これらの API 固有の動作は変更されません。たとえば、window.open() は以前と同じようにユーザー アクティベーションを使用しており、Navigator.prototype.vibrate() は、フレーム(またはそのいずれかのサブフレーム)がユーザー アクションを確認したことがある場合は引き続き有効です。そのため、ユーザーのアクティベーションごとに許可されるポップアップは 1 つだけです。

変更内容

  • User Activation v2 は、フレーム境界を越えたユーザー アクティベーションの可視性の概念を形式化しました。ユーザーが特定のフレームを操作すると、起点に関係なく、含まれるすべてのフレーム(およびそれらのフレームのみ)がアクティブになります。(Chrome 72 では、すべての同一オリジン フレームに表示を拡大する一時的な回避策を実施しています。この回避策は、ユーザー アクティベーションをサブフレームに明示的に渡す方法が得られ次第、削除する予定です)。
  • 有効化制限された API が、有効化されたフレームからイベント ハンドラ コード外から呼び出された場合、ユーザーの有効化状態が「アクティブ」である(期限切れや消費されていないなど)限り機能します。User Activation v2 以前は、無条件に失敗していました。
  • 有効期限内の複数の未使用のユーザー インタラクションは、最後のインタラクションに対応する 1 つのアクティベーションに融合します。

アクティベーション ゲート型 API における整合性の例

以下に、ユーザー アクティベーション v2 がアクティベーション制御型 API の動作の一貫性を保つ仕組みを示すポップアップ ウィンドウ(window.open() を使用して開きます)を含む 2 つの例を示します。

setTimeout() 通話のチェーン

この例は setTimeout() デモからのものです。click ハンドラが 1 秒以内にポップアップを開こうとすると、コードがどのように遅延を「合成」したかにかかわらず、成功することが期待されます。ユーザー アクティベーション v2 はこの前提を満たしているため、次の各イベント ハンドラは、click でポップアップを開きます(100 ミリ秒の遅延)。

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

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

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

ユーザー アクティベーション v2 がない場合、2 番目のイベント ハンドラは、テストしたすべてのブラウザで失敗します。(最初の失敗も、場合によっては失敗します)。

クロスドメインの postMessage() 呼び出し

postMessage() デモの例を次に示します。クロスオリジン サブフレーム内の click ハンドラが、2 つのメッセージを親フレームに直接送信するとします。親フレームは、次のいずれかのメッセージを受信したときにポップアップを開ける必要があります(両方は不可)。

// 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 がない場合、親フレームは 2 番目のメッセージを受信したときにポップアップを開けません。最初のメッセージでさえ、別のクロスオリジン フレームに「チェーンされている」場合(つまり、最初のレシーバーが別のメッセージを転送する場合)は失敗します。

これは、元の形式とチェーンの両方で、ユーザー アクティベーション v2 で機能します。