Controlled Frame

Demián Renzulli
Demián Renzulli
Simon Hangl
Simon Hangl

<iframe> 要素は通常、ブラウジング コンテキスト内に外部リソースを埋め込むために使用されます。iframe は、クロスオリジンの埋め込みコンテンツをホストページから分離することで、ウェブのセキュリティ ポリシーを適用します。このアプローチは、オリジン間の安全な境界を確保することでセキュリティを強化しますが、一部のユースケースが制限されます。たとえば、教師がナビゲーション イベントをトリガーして教室の画面にウェブページを表示するなど、ユーザーがさまざまなソースからコンテンツを動的に読み込んで管理する必要がある場合があります。ただし、多くのウェブサイトでは、X-Frame-Options やコンテンツ セキュリティ ポリシー(CSP)などのセキュリティ ヘッダーを使用して、iframe への埋め込みを明示的にブロックしています。また、iframe の制限により、埋め込みページが埋め込みコンテンツのナビゲーションや動作を直接管理することはできません。

Controlled Frame API は、制限の厳しい埋め込みポリシーが適用されている場合でも、任意のウェブ コンテンツを読み込めるようにすることで、この制限に対処します。この API は、分離されたウェブ アプリケーション(IWA)でのみ利用できます。IWA には、ユーザーとデベロッパーの両方を潜在的なリスクから保護するための追加のセキュリティ対策が組み込まれています。

Controlled Frame を実装する

制御フレームを使用する前に、機能する IWA を設定する必要があります。その後、制御されたフレームをページに統合できます。

権限ポリシーを追加する

制御フレームを使用するには、IWA マニフェストに値 "controlled-frame"permissions_policy フィールドを追加して、対応する権限を有効にします。また、cross-origin-isolated キーを含めることもできます。このキーは Controlled Frames に固有のものではありませんが、すべての IWA で必要であり、ドキュメントがクロスオリジン分離を必要とする API にアクセスできるかどうかを決定します。

{
   ...
  "permissions_policy": {
     ...
     "controlled-frame": ["self"],
     "cross-origin-isolated": ["self"]
     ...
  }
   ...
}

独立したウェブアプリ(IWA)のマニフェストの controlled-frame キーは、権限ポリシーの許可リストを定義し、どのオリジンが Controlled Frame を使用できるかを指定します。マニフェストは Permissions Policy の構文を完全にサポートしており、* などの値、特定のオリジン、selfsrc などのキーワードを使用できますが、IWA 固有の API を他のオリジンに委任することはできません。許可リストにワイルドカードや外部オリジンが含まれていても、これらの権限は controlled-frame などの IWA 機能には適用されません。標準のウェブアプリとは異なり、IWA ではポリシーで制御されるすべての機能がデフォルトで none に設定されるため、明示的な宣言が必要です。IWA 固有の機能の場合、これは self(IWA 独自のオリジン)や src(埋め込みフレームのオリジン)などの値のみが機能的に有効であることを意味します。

Controlled Frame 要素を追加する

HTML に <controlledframe> 要素を挿入して、IWA 内にサードパーティのコンテンツを埋め込みます。

<controlledframe id="controlledframe_1" src="https://example.com">
</controlledframe>

オプションの partition 属性は、埋め込みコンテンツのストレージ パーティショニングを構成します。これにより、Cookie やローカル ストレージなどのデータを分離して、セッション間でデータを保持できます。

例: インメモリストレージ パーティション

"session1" という名前のインメモリ ストレージ パーティションを使用して、Controlled Frame を作成します。このパーティションに保存されたデータ(Cookie や localStorage など)は、フレームが破棄されるか、アプリケーション セッションが終了するとクリアされます。

<controlledframe id="controlledframe_1" src="https://example.com">
</controlledframe>

例: 永続ストレージ パーティション

"user_data" という名前の永続ストレージ パーティションを使用して、制御されたフレームを作成します。接頭辞 "persist:" は、このパーティションに保存されたデータがディスクに保存され、アプリケーション セッション間で利用可能になることを保証します。

<controlledframe id="frame_2" src="..." partition="persist:user_data">
</controlledframe>

要素リファレンスを取得する

<controlledframe> 要素への参照を取得して、標準の HTML 要素と同じように操作できるようにします。

const controlledframe = document.getElementById('controlledframe_1');

一般的なシナリオとユースケース

原則として、不要な複雑さを回避しながら、ニーズを満たす最適なテクノロジーを選択します。近年、プログレッシブ ウェブアプリ(PWA)ネイティブ アプリとの差を縮め、強力なウェブ エクスペリエンスを実現しています。ウェブ アプリケーションでサードパーティのコンテンツを埋め込む必要がある場合は、まず通常の <iframe> アプローチを検討することをおすすめします。要件が iframe の機能を超える場合は、IWA の Controlled Frames が最適な代替手段となる可能性があります。一般的なユースケースについては、以降のセクションで説明します。

第三者のウェブ コンテンツを埋め込む

多くのアプリケーションでは、ユーザー インターフェース内でサードパーティのコンテンツを読み込んで表示する機能が必要です。ただし、複数のウェブアプリの所有者が関与している場合(埋め込みアプリケーションでは一般的なシナリオ)、一貫したエンドツーエンドのポリシーを確立することは困難になります。たとえば、セキュリティ設定により、企業が正当な理由で特定のタイプのコンテンツを埋め込む必要がある場合でも、従来の <iframe> での埋め込みが禁止されることがあります。<iframe> 要素とは異なり、制御されたフレームはこのような制限を回避するように設計されています。これにより、標準の埋め込みが明示的に禁止されている場合でも、アプリケーションでコンテンツを読み込んで表示できます。

ユースケース

  • 授業でのプレゼンテーション: 教師が教室のタッチスクリーンを使用して、通常は iframe の埋め込みをブロックする教育リソースを切り替えます。
  • 小売店やショッピング モールのデジタル サイネージ: ショッピング モールのキオスクで、さまざまな店舗のウェブサイトが切り替わって表示される。Controlled Frames を使用すると、埋め込みが制限されている場合でも、これらのページが正しく読み込まれます。

コードサンプル

次の Controlled Frame API は、埋め込みコンテンツの管理に役立ちます。

ナビゲーション: Controlled Frame は、埋め込みコンテンツのナビゲーションとナビゲーション履歴をプログラムで管理および制御するための複数の方法を提供します。

src 属性は、フレームに表示されるコンテンツの URL を取得または設定します。これは HTML 属性と同じように機能します。

controlledframe.src = "https://example.com";

back() メソッドは、フレームの履歴を 1 つ戻ります。返された Promise は、ナビゲーションが成功したかどうかを示すブール値に解決されます。

document.getElementById('backBtn').addEventListener('click', () => {
controlledframe.back().then((success) => {
console.log(`Back navigation ${success ? 'succeeded' : 'failed'}`); }).catch((error) => {
   console.error('Error during back navigation:', error);
   });
});

forward() メソッドは、フレームの履歴を 1 ステップ進めます。返された Promise は、ナビゲーションが成功したかどうかを示すブール値に解決されます。

document.getElementById('forwardBtn').addEventListener('click', () => {
controlledframe.forward().then((success) => {
   console.log(`Forward navigation ${success ? 'succeeded' : 'failed'}`);
}).catch((error) => {
    console.error('Error during forward navigation:', error);
  });
});

reload() メソッドは、フレーム内の現在のページを再読み込みします。

document.getElementById('reloadBtn').addEventListener('click', () => {
   controlledframe.reload();
});

また、Controlled Frames は、ナビゲーション リクエストのライフサイクル全体(開始、リダイレクト、コンテンツの読み込み、完了、中止)をトラッキングできるイベントを提供します。

  • loadstart: フレーム内でナビゲーションが開始されたときに発生します。
  • loadcommit: ナビゲーション リクエストが処理され、メイン ドキュメントのコンテンツの読み込みが開始されたときに発生します。
  • contentload: メイン ドキュメントとその重要なリソースの読み込みが完了したときに発生します(DOMContentLoaded と同様)。
  • loadstop: ページのリソース(サブフレーム、画像など)がすべて読み込まれたときに発動します。
  • loadabort: ナビゲーションが中止された場合(ユーザー操作や別のナビゲーションの開始など)に発生します。
  • loadredirect: ナビゲーション中にサーバーサイドのリダイレクトが発生したときに発生します。
controlledframe.addEventListener('loadstart', (event) => {
   console.log('Navigation started:', event.url);
   // Example: Show loading indicator
 });
controlledframe.addEventListener('loadcommit', (event) => {
   console.log('Navigation committed:', event.url);
 });
controlledframe.addEventListener('contentload', (event) => {
   console.log('Content loaded for:', controlledframe.src);
   // Example: Hide loading indicator, maybe run initial script
 });
controlledframe.addEventListener('loadstop', (event) => {
   console.log('All resources loaded for:', controlledframe.src);
 });
controlledframe.addEventListener('loadabort', (event) => {
   console.warn(`Navigation aborted: ${event.url}, Reason: ${event.detail.reason}`);
 });
controlledframe.addEventListener('loadredirect', (event) => {
   console.log(`Redirect detected: ${event.oldUrl} -> ${event.newUrl}`);
});

また、制御されたフレーム内で読み込まれたコンテンツによって開始された特定のインタラクションやリクエスト(ダイアログを開く、権限をリクエストする、新しいウィンドウを開くなど)をモニタリングし、必要に応じて阻止することもできます。

  • dialog: 埋め込みコンテンツがダイアログ(アラート、確認、プロンプト)を開こうとしたときに発生します。詳細を受け取り、返信できます。
  • consolemessage: フレーム内でメッセージがコンソールに記録されたときに発生します。
  • permissionrequest: 埋め込みコンテンツが権限(位置情報や通知など)をリクエストしたときに発生します。詳細が表示され、リクエストを許可または拒否できます。
  • newwindow: 埋め込みコンテンツが新しいウィンドウまたはタブを開こうとしたときに(たとえば、window.open または target="_blank" を含むリンクで)発火します。詳細を受け取り、アクションを処理またはブロックできます。
controlledframe.addEventListener('dialog', (event) => {
   console.log(Dialog opened: Type=${event.messageType}, Message=${event.messageText});
   // You will need to respond, e.g., event.dialog.ok() or .cancel()
 });

controlledframe.addEventListener('consolemessage', (event) => {
   console.log(Frame Console [${event.level}]: ${event.message});
 });

controlledframe.addEventListener('permissionrequest', (event) => {
   console.log(Permission requested: Type=${event.permission});
   // You must respond, e.g., event.request.allow() or .deny()
   console.warn('Permission request needs handling - Denying by default');
   if (event.request && event.request.deny) {
     event.request.deny();
   }
});

controlledframe.addEventListener('newwindow', (event) => {
   console.log(New window requested: URL=${event.targetUrl}, Name=${event.name});
   // Decide how to handle this, e.g., open in a new controlled frame and call event.window.attach(), ignore, or block
   console.warn('New window request needs handling - Blocking by default');
 });

制御されたフレームの独自のレンダリング状態(ディメンションやズームレベルの変更など)に関連する変更を通知する状態変更イベントもあります。

  • sizechanged: フレームのコンテンツの寸法が変更されたときに発生します。
  • zoomchange: フレームのコンテンツのズームレベルが変更されたときに発生します。
controlledframe.addEventListener('sizechanged', (event) => {
  console.log(Frame size changed: Width=${event.width}, Height=${event.height});
});

controlledframe.addEventListener('zoomchange', (event) => {
  console.log(Frame zoom changed: Factor=${event.newZoomFactor});
});

ストレージ メソッド: Controlled Frames は、フレームのパーティション内に保存されたデータを管理するための API を提供します。

clearData() を使用して保存されているすべてのデータを削除します。これは、ユーザー セッション後にフレームをリセットしたり、クリーンな状態を確保したりする場合に特に便利です。このメソッドは、オペレーションが完了すると解決される Promise を返します。オプションの構成オプションも指定できます。

  • types: 削除するデータの種類を指定する文字列の配列(['cookies', 'localStorage', 'indexedDB'] など)。省略すると、通常は該当するすべてのデータ型が削除されます。
  • options: クリア処理を制御します。たとえば、since プロパティ(エポックからのミリ秒単位のタイムスタンプ)を使用して時間範囲を指定し、その時間以降に作成されたデータのみをクリアします。

例: Controlled Frame に関連付けられているすべてのストレージをクリアする

function clearAllPartitionData() {
   console.log('Clearing all data for partition:', controlledframe.partition);
   controlledframe.clearData()
     .then(() => {
       console.log('Partition data cleared successfully.');
     })
     .catch((error) => {
       console.error('Error clearing partition data:', error);
     });
}

例: 過去 1 時間以内に作成された Cookie と localStorage のみをクリアする

function clearRecentCookiesAndStorage() {
   const oneHourAgo = Date.now() - (60 * 60 * 1000);
   const dataTypesArray = ['cookies', 'localStorage'];
   const dataTypesToClearObject = {};
   for (const type of dataTypesArray) {
      dataTypesToClearObject[type] = true;
   }
   const clearOptions = { since: oneHourAgo };
   console.log(`Clearing ${dataTypesArray.join(', ')} since ${new    Date(oneHourAgo).toISOString()}`); controlledframe.clearData(clearOptions, dataTypesToClearObject) .then(() => {
   console.log('Specified partition data cleared successfully.');
}).catch((error) => {
   console.error('Error clearing specified partition data:', error);
});
}

サードパーティ アプリケーションを拡張または変更する

Controlled Frame は、単なる埋め込みだけでなく、埋め込み側の IWA が埋め込まれたサードパーティのウェブ コンテンツを制御するためのメカニズムも提供します。埋め込みコンテンツ内でスクリプトを実行したり、ネットワーク リクエストをインターセプトしたり、デフォルトのコンテキスト メニューをオーバーライドしたりできます。これらはすべて、安全な隔離環境で行われます。

ユースケース

  • サードパーティ サイト全体でブランディングを適用する: 埋め込みウェブサイトにカスタム CSS と JavaScript を挿入して、統一されたビジュアル テーマを適用します。
  • ナビゲーションとリンクの動作を制限する: スクリプト挿入により、特定の <a> タグの動作をインターセプトまたは無効にします。
  • クラッシュや無操作後の復元を自動化する: 埋め込みコンテンツの障害状態(空白の画面、スクリプト エラーなど)をモニタリングし、タイムアウト後にセッションをプログラムで再読み込みまたはリセットします。

コードサンプル

スクリプト インジェクション: executeScript() を使用して、制御されたフレームに JavaScript を挿入できます。これにより、動作のカスタマイズ、オーバーレイの追加、埋め込まれたサードパーティ ページからのデータの抽出が可能になります。インライン コードを文字列として指定するか、1 つ以上のスクリプト ファイルを参照できます(IWA パッケージ内の相対パスを使用)。このメソッドは、スクリプトの実行結果(通常は最後のステートメントの値)に解決される Promise を返します。

document.getElementById('scriptBtn').addEventListener('click', () => {
   controlledframe.executeScript({
      code: `document.body.style.backgroundColor = 'lightblue';
             document.querySelectorAll('a').forEach(link =>    link.style.pointerEvents = 'none');
             document.title; // Return a value
            `,
      // You can also inject files:
      // files: ['./injected_script.js'],
}) .then((result) => {
   // The result of the last statement in the script is usually returned.
   console.log('Script execution successful. Result (e.g., page title):', result); }).catch((error) => {
   console.error('Script execution failed:', error);
   });
});

スタイル挿入: insertCSS() を使用して、制御されたフレーム内で読み込まれたページにカスタム スタイルを適用します。

document.getElementById('cssBtn').addEventListener('click', () => {
  controlledframe.insertCSS({
    code: `body { font-family: monospace; }`
    // You can also inject files:
    // files: ['./injected_styles.css']
  })
  .then(() => {
    console.log('CSS injection successful.');
  })
  .catch((error) => {
    console.error('CSS injection failed:', error);
  });
});

ネットワーク リクエストのインターセプト: WebRequest API を使用して、埋め込みページからのネットワーク リクエストを監視し、必要に応じて変更します(リクエストのブロック、ヘッダーの変更、使用状況のロギングなど)。

// Get the request object
const webRequest = controlledframe.request;

// Create an interceptor for a specific URL pattern
const interceptor = webRequest.createWebRequestInterceptor({
  urlPatterns: ["*://evil.com/*"],
  blocking: true,
  includeHeaders: "all"
});

// Add a listener to block the request
interceptor.addEventListener("beforerequest", (event) => {
  console.log('Blocking request to:', event.url);
  event.preventDefault();
});

// Add a listener to modify request headers
interceptor.addEventListener("beforesendheaders", (event) => {
  console.log('Modifying headers for:', event.url);
  const newHeaders = new Headers(event.headers);
  newHeaders.append('X-Custom-Header', 'MyValue');
  event.setRequestHeaders(newHeaders);
});

カスタム コンテキスト メニューの追加: contextMenus API を使用して、埋め込みフレーム内のカスタム右クリック メニューを追加、削除、処理します。この例では、制御されたフレーム内にカスタムの [選択をコピー] メニューを追加する方法を示します。テキストが選択された状態でユーザーが右クリックすると、メニューが表示されます。クリックすると、選択したテキストがクリップボードにコピーされ、埋め込みコンテンツ内で簡単かつユーザーフレンドリーな操作が可能になります。

const menuItemProperties = {
  id: "copy-selection",
  title: "Copy selection",
  contexts: ["selection"],
  documentURLPatterns: [new URLPattern({ hostname: '*.example.com'})]
};

// Create the context menu item using a promise
try {
  await controlledframe.contextMenus.create(menuItemProperties);
  console.log(`Context menu item "${menuItemProperties.id}" created successfully.`);
} catch (error) {
  console.error(`Failed to create context menu item:`, error);
}

// Add a standard event listener for the 'click' event
controlledframe.contextMenus.addEventListener('click', (event) => {
    if (event.menuItemId === "copy-selection" && event.selectionText) {
        navigator.clipboard.writeText(event.selectionText)
          .then(() => console.log("Text copied to clipboard."))
          .catch(err => console.error("Failed to copy text:", err));
    }
});

デモ

Controlled Frame のメソッドの概要については、Controlled Frame のデモをご覧ください。

Controlled Frame のデモ

また、IWA Kitchen Sink には、複数のタブを備えたアプリが用意されています。各タブでは、Controlled Frames や Direct Sockets など、さまざまな IWA API がデモされています。

IWA Kitchen Sink

まとめ

Controlled Frame は、独立したウェブアプリ(IWA)でサードパーティのウェブ コンテンツを埋め込み、拡張し、操作するための強力で安全な方法を提供します。iframe の制限を克服することで、埋め込みコンテンツ内でのスクリプトの実行、ネットワーク リクエストのインターセプト、カスタム コンテキスト メニューの実装などの新しい機能が実現します。これらはすべて、厳格な分離境界を維持しながら行われます。ただし、これらの API は埋め込みコンテンツを詳細に制御できるため、追加のセキュリティ制約が伴い、ユーザーとデベロッパーの両方に対してより強力な保証を適用するように設計された IWA 内でのみ使用できます。ほとんどのユースケースでは、まず標準の <iframe> 要素の使用を検討する必要があります。これは、多くのシナリオで十分なシンプルさを備えています。Controlled Frames は、埋め込み制限によって iframe ベースのソリューションがブロックされる場合や、必要な制御機能やインタラクション機能が不足している場合に評価する必要があります。キオスク エクスペリエンスの構築、サードパーティ製ツールの統合、モジュラー プラグイン システムの設計など、どのような場合でも、Controlled Frames を使用すると、構造化された権限付与済みの安全な環境で、きめ細かい制御が可能になります。そのため、Controlled Frames は次世代の高度なウェブ アプリケーションにおいて重要なツールとなります。

その他のリソース