HTML não corrigido na API Async Clipboard

No Chrome 120, uma nova opção unsanitized está disponível na API Async Clipboard. Essa opção pode ajudar em situações especiais com HTML, em que você precisa colar o conteúdo da área de transferência de forma idêntica ao que estava quando foi copiado. Isso significa que não há nenhuma etapa de limpeza intermediária que os navegadores geralmente aplicam, e por bons motivos. Saiba como usá-la neste guia.

Ao trabalhar com a API Async Clipboard, na maioria dos casos, os desenvolvedores não precisam se preocupar com a integridade do conteúdo na área de transferência e podem presumir que o que eles gravam na área de transferência (cópia) são o mesmo que vão receber quando ler os dados da área de transferência (colar).

Isso é definitivamente verdade para textos. Cole o código abaixo no console do DevTools e redefina a página imediatamente. O setTimeout() é necessário para que você tenha tempo suficiente para focar a página, o que é um requisito da API Async Clipboard. Como você pode ver, a entrada é exatamente igual à saída.

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

Com imagens, é um pouco diferente. Para evitar os chamados ataques de bomba de compactação, os navegadores recodificam imagens como PNGs, mas as imagens de entrada e de saída são visualmente iguais, pixel por 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);

Mas o que acontece com o texto HTML? Como você pode imaginar, com HTML, a situação é diferente. Aqui, o navegador higieniza o código HTML para evitar problemas, por exemplo, removendo tags <script> do código HTML (e outras como <meta>, <head> e <style>) e inserindo o CSS. Considere o exemplo a seguir e teste no Console do DevTools. Você vai notar que a saída é bastante diferente da entrada.

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

A limpeza de HTML geralmente é uma coisa boa. Não se exponha a problemas de segurança ao permitir HTML não higienizado na maioria dos casos. No entanto, há cenários em que o desenvolvedor sabe exatamente o que está fazendo e em que a integridade do HTML de entrada e saída é crucial para o funcionamento correto do app. Nessas circunstâncias, você tem duas opções:

  1. Se você controlar a etapa de copiar e colar, por exemplo, se copiar de dentro do app para depois colar no app, use formatos personalizados da Web para a API Async Clipboard. Pare de ler aqui e confira o artigo vinculado.
  2. Se você controlar apenas a parte de colagem no app, mas não a de cópia, talvez porque a operação de cópia ocorra em um app nativo que não oferece suporte a formatos personalizados da Web, use a opção unsanitized, que é explicada no restante deste artigo.

A limpeza inclui coisas como remover tags script, alinhar estilos e garantir que o HTML esteja bem formado. Esta lista não é abrangente, e mais etapas podem ser adicionadas no futuro.

Cópia e colagem de HTML não higienizado

Quando você write() (copia) HTML para a área de transferência com a API Async Clipboard, o navegador garante que ele esteja bem formado executando-o em um analisador DOM e serializando a string HTML resultante, mas nenhuma limpeza está acontecendo nesta etapa. Não é necessário fazer nada. Quando você read() HTML colocado na área de transferência por outro aplicativo e seu app da Web está ativando a opção de receber o conteúdo de fidelidade total e precisa realizar a higienização no seu próprio código, é possível transmitir um objeto de opções para o método read() com uma propriedade unsanitized e um valor de ['text/html']. Isolado, ele tem esta aparência: navigator.clipboard.read({ unsanitized: ['text/html'] }). O exemplo de código abaixo é quase o mesmo que o mostrado anteriormente, mas desta vez com a opção unsanitized. Ao tentar no console do DevTools, você vai notar que a entrada e a saída são as mesmas.

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

Suporte a navegadores e detecção de recursos

Não há uma maneira direta de verificar se o recurso tem suporte. Portanto, a detecção de recursos é baseada na observação do comportamento. Portanto, o exemplo a seguir depende da detecção de se uma tag <style> sobrevive, o que indica suporte, ou se está sendo inline, o que indica a falta de suporte. Para que isso funcione, a página precisa ter recebido a permissão de área de transferência.

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

Demonstração

Para conferir a opção unsanitized em ação, consulte a demonstração no Glitch e confira o código-fonte.

Conclusões

Conforme descrito na introdução, a maioria dos desenvolvedores nunca precisa se preocupar com a higienização da área de transferência e pode trabalhar apenas com as opções de higienização padrão feitas pelo navegador. Para os raros casos em que os desenvolvedores precisam se importar, a opção unsanitized existe.

Agradecimentos

Este artigo foi revisado por Anupam Snigdha e Rachel Andrew. Ela foi especificada e implementada pela equipe do Microsoft Edge.