Cadre contrôlé

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

L'élément <iframe> est généralement utilisé pour intégrer des ressources externes dans un contexte de navigation. Les iFrames appliquent les règles de sécurité du Web en isolant le contenu intégré multi-origines de la page hôte et inversement. Bien que cette approche améliore la sécurité en garantissant une limite sécurisée entre les origines, elle limite certains cas d'utilisation. Par exemple, les utilisateurs peuvent avoir besoin de charger et de gérer dynamiquement du contenu provenant de différentes sources, comme un enseignant qui déclenche un événement de navigation pour afficher une page Web sur un écran de salle de classe. Toutefois, de nombreux sites Web bloquent explicitement l'intégration dans des iFrames à l'aide d'en-têtes de sécurité tels que X-Frame-Options et Content Security Policy (CSP). De plus, les restrictions liées aux iFrames empêchent les pages intégrées de gérer directement la navigation ou le comportement du contenu intégré.

L'API Controlled Frame résout cette limitation en permettant le chargement de n'importe quel contenu Web, même s'il applique des règles d'intégration restrictives. Cette API est exclusivement disponible dans les applications Web isolées (AWI), qui intègrent des mesures de sécurité supplémentaires pour protéger les utilisateurs et les développeurs contre les risques potentiels.

Implémenter les frames contrôlées

Avant d'utiliser un cadre contrôlé, vous devez configurer une IWA fonctionnelle. Vous pouvez ensuite intégrer des frames contrôlés à vos pages.

Ajouter une règle d'autorisation

Pour utiliser les frames contrôlés, activez l'autorisation correspondante en ajoutant un champ permissions_policy avec la valeur "controlled-frame" à votre fichier manifeste d'application Web installable. Ajoutez également la clé cross-origin-isolated. Cette clé n'est pas spécifique aux cadres contrôlés, mais elle est requise pour toutes les IWA et détermine si le document peut accéder aux API qui nécessitent l'isolation multi-origine.

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

La clé controlled-frame dans le fichier manifeste d'une application Web isolée (AWI) définit une liste d'autorisation de la règle d'autorisation, qui spécifie les origines pouvant utiliser les cadres contrôlés. Bien que le fichier manifeste soit compatible avec la syntaxe complète de la règle d'autorisation, ce qui permet d'utiliser des valeurs telles que *, des origines spécifiques ou des mots clés tels que self et src, il est essentiel de noter que les API spécifiques aux AWP ne peuvent pas être déléguées à d'autres origines. Même si la liste d'autorisation inclut un caractère générique ou des origines externes, ces autorisations ne s'appliqueront pas aux fonctionnalités IWA telles que controlled-frame. Contrairement aux applications Web standards, les PWA définissent par défaut toutes les fonctionnalités contrôlées par des règles sur "aucune", ce qui nécessite des déclarations explicites. Pour les fonctionnalités spécifiques aux AWI, cela signifie que seules les valeurs telles que self (l'origine de l'AWI) ou src (l'origine d'un frame intégré) sont fonctionnellement efficaces.

Ajouter un élément "Cadre contrôlé"

Insérez un élément <controlledframe> dans votre code HTML pour intégrer du contenu tiers dans votre PWA.

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

L'attribut partition facultatif configure le partitionnement du stockage pour le contenu intégré, ce qui vous permet d'isoler les données telles que les cookies et le stockage local pour conserver les données d'une session à l'autre.

Exemple : Partition de stockage en mémoire

Créez un ControlledFrame à l'aide d'une partition de stockage en mémoire nommée "session1". Les données stockées dans cette partition (par exemple, les cookies et le localStorage) seront effacées lorsque le frame sera détruit ou que la session de l'application se terminera.

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

Exemple : Partition de stockage persistant

Créez un frame contrôlé à l'aide d'une partition de stockage persistant nommée "user_data". Le préfixe "persist:" garantit que les données stockées dans cette partition sont enregistrées sur le disque et seront disponibles dans toutes les sessions de l'application.

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

Obtenir une référence d'élément

Obtenez une référence à l'élément <controlledframe> pour pouvoir interagir avec lui comme avec n'importe quel élément HTML standard :

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

Scénarios et cas d'utilisation fréquents

En règle générale, choisissez la technologie la mieux adaptée à vos besoins tout en évitant toute complexité inutile. Ces dernières années, les progressive web apps (PWA) ont comblé le fossé avec les applications natives, permettant de puissantes expériences Web. Si une application Web doit intégrer du contenu tiers, il est recommandé d'explorer d'abord l'approche <iframe> habituelle. Si les exigences dépassent les capacités des iFrames, les frames contrôlés sur les IWA peuvent être la meilleure alternative. Les cas d'utilisation courants sont décrits dans les sections suivantes.

Intégrer du contenu Web tiers

De nombreuses applications doivent pouvoir charger et afficher du contenu tiers dans leur interface utilisateur. Toutefois, lorsqu'il y a plusieurs propriétaires d'applications Web (ce qui est courant avec les applications intégrées), il devient difficile d'établir des règles cohérentes de bout en bout. Par exemple, les paramètres de sécurité peuvent empêcher un <iframe> traditionnel d'intégrer certains types de contenu, même lorsque les entreprises ont un besoin légitime de le faire. Contrairement aux éléments <iframe>, les cadres contrôlés sont conçus pour contourner ces restrictions, ce qui permet aux applications de charger et d'afficher du contenu même s'il interdit explicitement l'intégration standard.

Cas d'utilisation

  • Présentations en classe : un enseignant utilise un écran tactile en classe pour passer d'une ressource pédagogique à une autre, alors que l'intégration d'iframe serait normalement bloquée.
  • Signalétique numérique dans les magasins ou les centres commerciaux : un kiosque de centre commercial fait défiler les sites Web de différents magasins. Les frames contrôlés permettent à ces pages de se charger correctement, même si elles limitent l'intégration.

Exemples de code

Les API Controlled Frame suivantes sont utiles pour gérer le contenu intégré.

Navigation : les frames contrôlés offrent plusieurs méthodes pour gérer et contrôler de manière programmatique la navigation et l'historique de navigation du contenu intégré.

L'attribut src obtient ou définit l'URL du contenu affiché dans le frame, de la même manière que l'attribut HTML.

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

La méthode back() revient à l'étape précédente de l'historique du frame. La promesse renvoyée est résolue en une valeur booléenne indiquant si la navigation a réussi.

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

La méthode forward() permet d'avancer d'une étape dans l'historique du frame. La promesse renvoyée est résolue en une valeur booléenne indiquant si la navigation a réussi.

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

La méthode reload() recharge la page actuelle dans le frame.

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

De plus, les frames contrôlés fournissent des événements qui vous permettent de suivre l'intégralité du cycle de vie des requêtes de navigation, de l'initiation et des redirections au chargement, à la finalisation ou à l'abandon du contenu.

  • loadstart : déclenché lorsqu'une navigation commence dans le frame.
  • loadcommit : déclenché lorsque la requête de navigation a été traitée et que le contenu du document principal commence à se charger.
  • contentload : déclenché lorsque le document principal et ses ressources essentielles ont fini de se charger (similaire à DOMContentLoaded).
  • loadstop : déclenché lorsque toutes les ressources de la page (y compris les sous-frames et les images) ont fini de se charger.
  • loadabort : déclenché si une navigation est abandonnée (par exemple, par une action de l'utilisateur ou le démarrage d'une autre navigation).
  • loadredirect : déclenché lorsqu'une redirection côté serveur se produit lors de la navigation.
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}`);
});

Vous pouvez également surveiller et potentiellement intercepter des interactions ou des requêtes spécifiques initiées par le contenu chargé dans le frame contrôlé, comme les tentatives d'ouverture de boîtes de dialogue, de demande d'autorisations ou d'ouverture de nouvelles fenêtres.

  • dialog : déclenché lorsque le contenu intégré tente d'ouvrir une boîte de dialogue (alerte, confirmation, invite). Vous recevez des informations et pouvez y répondre.
  • consolemessage : déclenché lorsqu'un message est consigné dans la console du frame.
  • permissionrequest : déclenché lorsque le contenu intégré demande une autorisation (par exemple, la géolocalisation et les notifications). Vous recevez des informations détaillées et pouvez autoriser ou refuser la demande.
  • newwindow : déclenché lorsque le contenu intégré tente d'ouvrir une nouvelle fenêtre ou un nouvel onglet (par exemple, avec window.open ou un lien avec target="_blank"). Vous recevez des informations et pouvez gérer ou bloquer l'action.
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');
 });

Il existe également des événements de changement d'état qui vous informent des modifications liées à l'état de rendu du frame contrôlé lui-même, comme les modifications de ses dimensions ou de son niveau de zoom.

  • sizechanged : déclenché lorsque les dimensions du contenu du frame changent.
  • zoomchange : déclenché lorsque le niveau de zoom du contenu du frame change.
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});
});

Méthodes de stockage : les cadres contrôlés proposent des API pour gérer les données stockées dans la partition d'un cadre.

Utilisez clearData() pour supprimer toutes les données stockées. Cette méthode est particulièrement utile pour réinitialiser le frame après une session utilisateur ou pour garantir un état propre. La méthode renvoie une promesse qui est résolue une fois l'opération terminée. Vous pouvez également fournir des options de configuration facultatives :

  • types : tableau de chaînes spécifiant les types de données à effacer (par exemple, ['cookies', 'localStorage', 'indexedDB']). S'il est omis, tous les types de données applicables sont généralement effacés.
  • options : contrôle le processus d'effacement, par exemple en spécifiant une plage de temps à l'aide d'une propriété "since" (code temporel en millisecondes depuis l'epoch) pour n'effacer que les données créées après cette heure.

Exemple : Effacer tout l'espace de stockage associé au Frame contrôlé

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

Exemple : Effacer uniquement les cookies et le localStorage créés au cours de la dernière heure

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

Étendre ou modifier des applications tierces

Au-delà de l'intégration simple, les frames contrôlés offrent des mécanismes permettant à l'AWI d'intégration d'exercer un contrôle sur le contenu Web tiers intégré. Vous pouvez exécuter des scripts dans le contenu intégré, intercepter les requêtes réseau et remplacer les menus contextuels par défaut, le tout dans un environnement sécurisé et isolé.

Cas d'utilisation

  • Appliquer le branding sur les sites tiers : injectez du code CSS et JavaScript personnalisé dans les sites Web intégrés pour appliquer un thème visuel unifié.
  • Restreindre la navigation et le comportement des liens : interceptez ou désactivez certains comportements de balise <a> avec l'injection de script.
  • Automatiser la récupération après des plantages ou une inactivité : surveillez le contenu intégré pour détecter les états d'échec (par exemple, écran vide, erreurs de script) et rechargez ou réinitialisez la session de manière programmatique après un délai d'inactivité.

Exemples de code

Injection de script : utilisez executeScript() pour injecter du code JavaScript dans le frame contrôlé, ce qui vous permet de personnaliser le comportement, d'ajouter des calques ou d'extraire des données à partir de pages tierces intégrées. Vous pouvez fournir du code intégré sous forme de chaîne ou référencer un ou plusieurs fichiers de script (à l'aide de chemins d'accès relatifs dans le package IWA). La méthode renvoie une promesse qui se résout au résultat de l'exécution du script, généralement la valeur de la dernière instruction.

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

Injection de style : utilisez insertCSS() pour appliquer des styles personnalisés aux pages chargées dans un cadre contrôlé.

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

Interception des requêtes réseau : utilisez l'API WebRequest pour observer et potentiellement modifier les requêtes réseau de la page intégrée, par exemple en bloquant les requêtes, en modifiant les en-têtes ou en enregistrant l'utilisation.

// 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);
});

Ajouter des menus contextuels personnalisés : utilisez l'API contextMenus pour ajouter, supprimer et gérer des menus contextuels personnalisés dans le frame intégré. Cet exemple montre comment ajouter un menu "Copier la sélection" personnalisé dans un cadre contrôlé. Le menu s'affiche lorsque l'utilisateur sélectionne du texte et effectue un clic droit. En cliquant dessus, le texte sélectionné est copié dans le presse-papiers, ce qui permet des interactions simples et conviviales dans le contenu intégré.

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

Démo

Consultez la démonstration de Controlled Frame pour obtenir une présentation des méthodes de Controlled Frame.

Démo de Controlled Frame

Vous pouvez également utiliser IWA Kitchen Sink, qui propose une application avec plusieurs onglets, chacun présentant une API IWA différente, comme les frames contrôlés, les sockets directs, etc.

IWA Kitchen Sink

Conclusion

Les frames contrôlés offrent un moyen puissant et sécurisé d'intégrer, d'étendre et d'interagir avec du contenu Web tiers dans les applications Web isolées (AWI). En surmontant les limites des iframes, ils permettent de nouvelles fonctionnalités telles que l'exécution de scripts dans du contenu intégré, l'interception des requêtes réseau et l'implémentation de menus contextuels personnalisés, tout en maintenant des limites d'isolation strictes. Toutefois, comme ces API offrent un contrôle approfondi sur le contenu intégré, elles sont également soumises à des contraintes de sécurité supplémentaires et ne sont disponibles que dans les IWA, qui sont conçues pour offrir des garanties plus solides aux utilisateurs et aux développeurs. Dans la plupart des cas d'utilisation, les développeurs doivent d'abord envisager d'utiliser des éléments <iframe> standards, qui sont plus simples et suffisants dans de nombreux scénarios. Les cadres contrôlés doivent être évalués lorsque les solutions basées sur des iFrames sont bloquées par des restrictions d'intégration ou ne disposent pas des capacités de contrôle et d'interaction nécessaires. Que vous créiez des expériences de kiosque, que vous intégriez des outils tiers ou que vous conceviez des systèmes de plug-ins modulaires, les cadres contrôlés permettent un contrôle précis dans un environnement structuré, autorisé et sécurisé. Ils constituent donc un outil essentiel pour la prochaine génération d'applications Web avancées.

Autres ressources