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 idêntico ao usado quando ela foi copiada. Ou seja, sem nenhuma etapa de limpeza intermediária que os navegadores normalmente aplicam e por bons motivos. Saiba como usá-lo 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 gravam na área de transferência (cópia) é o mesmo que eles vão receber quando ler os dados da área de transferência (colar).

Isso é definitivamente verdade para o texto. Tente colar o código a seguir no Console do DevTools e mude o foco da página imediatamente. O setTimeout() é necessário para que você tenha tempo suficiente para focar a página, o que é um requisito da API AsyncClipboard. 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 exatamente as mesmas, 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);

O que acontece com o texto HTML? Como você deve ter adivinhado, com HTML, a situação é diferente. Nesse caso, o navegador limpa o código HTML para evitar que coisas ruins aconteçam, por exemplo, removendo tags <script> do código HTML (e outras como <meta>, <head> e <style>) e inserindo CSS in-line. Considere o exemplo a seguir e tente no Console do DevTools. Você notará que a saída difere significativamente 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 do HTML geralmente é algo bom. Não convém se expor a problemas de segurança permitindo HTML não corrigido 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 o final de copiar e colar, por exemplo, ao copiar do app para colar dentro dele, 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 extremidade de colagem no app, mas não o fim da cópia, talvez porque a operação de cópia aconteça 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 ações como remover tags script, estilos in-line e garantir que o HTML está bem formado. Esta lista não é abrangente, e mais etapas podem ser adicionadas no futuro.

Copiar e colar HTML não corrigido

Quando você write() (copia) o HTML para a área de transferência com a API Async Clipboard, o navegador verifica se ele foi bem formado, executando-o por um analisador DOM e serializando a string HTML resultante, mas nenhuma limpeza está acontecendo nesta etapa. Nenhuma ação é necessária. Quando você read() o HTML colocado na área de transferência por outro aplicativo e seu app da Web está ativando o conteúdo de fidelidade total e precisando realizar qualquer limpeza no seu próprio código, você pode transmitir um objeto de opções ao método read() com uma propriedade unsanitized e um valor de ['text/html']. Isoladamente, ela tem a seguinte aparência: navigator.clipboard.read({ unsanitized: ['text/html'] }). O exemplo de código abaixo é quase igual ao mostrado anteriormente, mas desta vez com a opção unsanitized. Ao testar no Console do DevTools, você verá 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 é compatível. Portanto, a detecção de recursos é baseada na observação do comportamento. Portanto, o exemplo a seguir depende da detecção do fato de uma tag <style> sobreviver, o que indica a compatibilidade, ou está sendo inline, o que indica não compatibilidade. Para que isso funcione, a página já precisa ter a permissão da á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 do Glitch e confira o código-fonte.

Conclusões

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

Agradecimentos

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