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

Mustaq Ahmed
Joe Medley
Joe Medley

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

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

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

User Activation v2 の仕組み

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

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

変更内容

  • 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 で機能します。