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

Mustaq Ahmed
Joe Medley
Joe Medley

悪意のあるスクリプトがポップアップやフルスクリーンなどの機密性の高い API を悪用しないように、ブラウザはユーザーの有効化によってこれらの API へのアクセスを制御します。ユーザーのアクティベーションは、ユーザーの操作に関連するブラウジング セッションの状態です。「アクティブ」状態は通常、ユーザーが現在ページを操作しているか、ページの読み込み後に操作を完了したことを意味します。ユーザー ジェスチャーは、同じ概念を表す一般的な用語ですが、誤解を招く可能性があります。たとえば、ユーザーによるスワイプやフリック操作はページをアクティブにしません。そのため、スクリプトから見ると、ユーザーによるアクティベーションとは見なされません。

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

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

User Activation v2 の仕組み

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

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

変更内容

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

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

以下に、ポップアップ ウィンドウ(window.open() を使用して開く)を使用した 2 つの例を示します。これらの例は、User Activation v2 によって、有効化制限付き API の動作が一貫する仕組みを示しています。

setTimeout() 通話のチェーン

この例は setTimeout() デモからのものです。click ハンドラが 1 秒以内にポップアップを開こうとした場合、コードが遅延をどのように「コンポーズ」しても、成功することが期待されます。User Activation v2 はこの要件を満たしているため、次の各イベント ハンドラは click でポップアップを開きます(100 ミリ秒の遅延あり)。

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

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

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

User Activation 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);
});

User Activation v2 がないと、親フレームは 2 つ目のメッセージを受信したときにポップアップを開くことができません。最初のメッセージが別のクロスオリジン フレームに「連結」されている場合(つまり、最初のレシーバーがメッセージを別のレシーバーに転送する場合)でも、最初のメッセージは失敗します。

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