Code HTML non rectifié dans l'API Async Clipboard

À partir de Chrome 120, une nouvelle option unsanitized est disponible dans l'API Async Clipboard. Cette option peut être utile dans les situations spéciales avec HTML, où vous devez coller le contenu du presse-papiers comme il était au moment de la copie. Autrement dit, sans étape de nettoyage intermédiaire que les navigateurs appliquent fréquemment, et à juste titre. Découvrez comment l'utiliser dans ce guide.

Dans la plupart des cas, lorsqu'ils utilisent l'API Async Clipboard, les développeurs n'ont pas à se soucier de l'intégrité du contenu dans le presse-papiers et peuvent supposer que ce qu'ils écrivent dans le presse-papiers (copie) est identique à ce qu'ils obtiendront lorsqu'ils liront les données du presse-papiers (coller).

C'est certainement vrai pour le texte. Essayez de coller le code suivant dans la console DevTools, puis recentrez la page immédiatement. (Le setTimeout() est nécessaire afin que vous ayez suffisamment de temps pour vous concentrer sur la page, ce qui est une exigence de l'API Async Clipboard.) Comme vous pouvez le constater, l'entrée est exactement la même que la sortie.

setTimeout(async () => {
  const input = 'Hello';
  await navigator.clipboard.writeText(input);
  const output = await navigator.clipboard.readText();
  console.log(input, output, input === output);
  // Logs "Hello Hello true".
}, 3000);

Pour les images, c'est un peu différent. Pour prévenir les attaques par bombe à compression, les navigateurs encodent à nouveau les images au format PNG, mais les images d'entrée et de sortie sont visuellement exactement identiques (pixel par pixel).

setTimeout(async () => {
  const dataURL =
    'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVQYV2NgYAAAAAMAAWgmWQ0AAAAASUVORK5CYII=';
  const input = await fetch(dataURL).then((response) => response.blob());
  await navigator.clipboard.write([
    new ClipboardItem({
      [input.type]: input,
    }),
  ]);
  const [clipboardItem] = await navigator.clipboard.read();
  const output = await clipboardItem.getType(input.type);
  console.log(input.size, output.size, input.type === output.type);
  // Logs "68 161 true".
}, 3000);

Mais qu'en est-il du texte HTML ? Comme vous l'avez peut-être deviné, avec HTML, la situation est différente. Ici, le navigateur nettoie le code HTML pour empêcher les activités malveillantes, par exemple en supprimant les balises <script> du code HTML (et d'autres éléments tels que <meta>, <head> et <style>) et en intégrant le code CSS. Essayez l'exemple suivant dans la console DevTools. Vous remarquerez que la sortie diffère assez considérablement de l'entrée.

setTimeout(async () => {
  const input = `<html>  
  <head>  
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />  
    <meta name="ProgId" content="Excel.Sheet" />  
    <meta name="Generator" content="Microsoft Excel 15" />  
    <style>  
      body {  
        font-family: HK Grotesk;  
        background-color: var(--color-bg);  
      }  
    </style>  
  </head>  
  <body>  
    <div>hello</div>  
  </body>  
</html>`;
  const inputBlob = new Blob([input], { type: 'text/html' });
  await navigator.clipboard.write([
    new ClipboardItem({
      'text/html': inputBlob,
    }),
  ]);
  const [clipboardItem] = await navigator.clipboard.read();
  const outputBlob = await clipboardItem.getType('text/html');
  const output = await outputBlob.text();
  console.log(input, output);
}, 3000);

En général, le nettoyage du code HTML est une bonne chose. Dans la plupart des cas, vous ne devez pas vous exposer à des problèmes de sécurité en autorisant le code HTML non rectifié. Toutefois, il peut arriver que le développeur sache exactement ce qu'il fait, et où l'intégrité du code HTML d'entrée et de sortie est essentielle au bon fonctionnement de l'application. Dans ces cas, deux possibilités s'offrent à vous:

  1. Si vous contrôlez à la fois le copier-coller (par exemple, si vous copiez des éléments depuis votre application pour ensuite les coller dans celle-ci), vous devez utiliser les formats Web personnalisés pour l'API Async Clipboard. Arrêtez votre lecture ici et consultez l'article en lien.
  2. Si vous ne contrôlez que la fin du collage dans votre application, mais pas la fin de la copie, peut-être parce que l'opération de copie a lieu dans une application native qui n'est pas compatible avec les formats Web personnalisés. Vous devez utiliser l'option unsanitized, qui est expliquée dans la suite de cet article.

Cette opération comprend, entre autres, la suppression des balises script, l'intégration de styles et le bon format du code HTML. Cette liste n'est pas exhaustive, et d'autres étapes pourraient être ajoutées ultérieurement.

Copier et coller du code HTML non rectifié

Lorsque vous write() (copiez) du code HTML dans le presse-papiers avec l'API Async Clipboard, le navigateur s'assure qu'il est bien formé en l'exécutant via un analyseur DOM et en sérialisant la chaîne HTML obtenue, mais aucune opération de nettoyage n'est effectuée à cette étape. Vous n'avez rien à faire. Lorsque vous read() du code HTML placé dans le presse-papiers par une autre application et que votre application Web choisit d'obtenir le contenu intégral et doit effectuer toute opération de nettoyage dans votre propre code, vous pouvez transmettre un objet d'options à la méthode read() avec une propriété unsanitized et une valeur de ['text/html']. Isolé, il se présente comme suit : navigator.clipboard.read({ unsanitized: ['text/html'] }). L'exemple de code ci-dessous est presque identique à celui présenté précédemment, mais cette fois avec l'option unsanitized. Si vous l'essayez dans la console des outils de développement, vous constaterez que l'entrée et la sortie sont identiques.

setTimeout(async () => {
  const input = `<html>  
  <head>  
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />  
    <meta name="ProgId" content="Excel.Sheet" />  
    <meta name="Generator" content="Microsoft Excel 15" />  
    <style>  
      body {  
        font-family: HK Grotesk;  
        background-color: var(--color-bg);  
      }  
    </style>  
  </head>  
  <body>  
    <div>hello</div>  
  </body>  
</html>`;
  const inputBlob = new Blob([input], { type: 'text/html' });
  await navigator.clipboard.write([
    new ClipboardItem({
      'text/html': inputBlob,
    }),
  ]);
  const [clipboardItem] = await navigator.clipboard.read({
    unsanitized: ['text/html'],
  });
  const outputBlob = await clipboardItem.getType('text/html');
  const output = await outputBlob.text();
  console.log(input, output);
}, 3000);

Prise en charge des navigateurs et détection des fonctionnalités

Il n'existe aucun moyen direct de vérifier si une fonctionnalité est compatible. La détection des fonctionnalités repose donc sur l'observation du comportement. Par conséquent, l'exemple suivant repose sur la détection du fait qu'une balise <style> survive, ce qui indique une compatibilité, ou est intégrée, ce qui signifie qu'elle n'est pas prise en charge. Notez que pour que cela fonctionne, la page doit déjà avoir obtenu l'autorisation du presse-papiers.

const supportsUnsanitized = async () => {
  const input = `<style>p{color:red}</style><p>a`;
  const inputBlob = new Blob([input], { type: 'text/html' });
  await navigator.clipboard.write([
    new ClipboardItem({
      'text/html': inputBlob,
    }),
  ]);
  const [clipboardItem] = await navigator.clipboard.read({
    unsanitized: ['text/html],
  });
  const outputBlob = await clipboardItem.getType('text/html');
  const output = await outputBlob.text();
  return /<style>/.test(output);
};

Démonstration

Pour voir l'option unsanitized en action, regardez la démonstration sur Glitch et consultez son code source.

Conclusions

Comme indiqué dans l'introduction, la plupart des développeurs n'auront jamais à se soucier de l'assainissement du presse-papiers et pourront simplement utiliser les choix de nettoyage par défaut du navigateur. Dans les rares cas où les développeurs doivent s'en soucier, l'option unsanitized existe.

Remerciements

Cet article a été examiné par Anupam Snigdha et Rachel Andrew. L'API a été spécifiée et mise en œuvre par l'équipe Microsoft Edge.