Необработанный HTML в API асинхронного буфера обмена

Начиная с Chrome 120, в API Async Clipboard доступна новая unsanitized опция. Эта опция может помочь в особых ситуациях с HTML, когда вам необходимо вставить содержимое буфера обмена, идентичное тому, каким оно было при копировании. То есть без какого-либо промежуточного этапа очистки, который браузеры обычно — и по уважительным причинам — применяют. Узнайте, как использовать его в этом руководстве.

При работе с Async Clipboard API в большинстве случаев разработчикам не нужно беспокоиться о целостности содержимого в буфере обмена и они могут предполагать, что то, что они записывают в буфер обмена (копируют), совпадает с тем, что они получат. когда они читают данные из буфера обмена (вставляют).

Это определенно верно для текста. Попробуйте вставить следующий код в консоль DevTools, а затем немедленно перефокусировать страницу. (Вызов setTimeout() необходим для того, чтобы у вас было достаточно времени, чтобы сфокусировать страницу, что является требованием API Async Clipboard.) Как видите, входные данные точно такие же, как выходные данные.

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

С изображениями дело обстоит немного иначе. Чтобы предотвратить так называемые атаки «сжимающей бомбы» , браузеры перекодируют изображения, такие как PNG, но входные и выходные изображения визуально абсолютно одинаковы, пиксель за пикселем.

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

Но что происходит с HTML-текстом? Как вы уже догадались, с HTML ситуация иная. Здесь браузер очищает HTML-код, чтобы предотвратить возникновение плохих вещей, например, удаляя теги <script> из HTML-кода (и другие, такие как <meta> , <head> и <style> ) и встраивая CSS. . Рассмотрите следующий пример и попробуйте его в консоли DevTools. Вы заметите, что выходные данные весьма существенно отличаются от входных.

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

Санитизация HTML в целом — это хорошо. Вы не хотите подвергать себя проблемам безопасности, разрешая в большинстве случаев несанкционированный HTML. Однако существуют сценарии, в которых разработчик точно знает, что он делает, и где целостность входного и выходного HTML имеет решающее значение для правильного функционирования приложения. В таких обстоятельствах у вас есть два варианта:

  1. Если вы управляете как копированием, так и окончанием вставки, например, если вы копируете из своего приложения, а затем аналогичным образом вставляете его в свое приложение, вам следует использовать пользовательские веб-форматы для Async Clipboard API . Прекратите читать здесь и проверьте связанную статью.
  2. Если вы управляете только окончанием вставки в своем приложении, но не окончанием копирования, возможно, потому, что операция копирования происходит в собственном приложении, которое не поддерживает пользовательские веб-форматы, вам следует использовать параметр unsanitized , который объясняется в остальной части статьи. Эта статья.

Очистка включает в себя такие вещи, как удаление тегов script , встраивание стилей и обеспечение правильного формата HTML. Этот список не является исчерпывающим, и в будущем могут быть добавлены дополнительные шаги.

Скопируйте и вставьте необработанный HTML-код.

Когда вы write() копируете) HTML в буфер обмена с помощью API Async Clipboard, браузер проверяет его правильность, пропуская его через анализатор DOM и сериализуя полученную строку HTML, но на этом этапе не происходит никакой очистки. Вам не нужно ничего делать. Когда вы read() HTML-код, помещенный в буфер обмена другим приложением, и ваше веб-приложение соглашается на получение полного содержимого и требует выполнения какой-либо очистки в вашем собственном коде, вы можете передать объект параметров методу read() со свойством unsanitized и значением ['text/html'] . По отдельности это выглядит так: navigator.clipboard.read({ unsanitized: ['text/html'] }) . Следующий пример кода ниже почти такой же, как показанный ранее, но на этот раз с unsanitized параметром. Когда вы попробуете это в консоли DevTools, вы увидите, что ввод и вывод одинаковы.

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

Поддержка браузера и обнаружение функций

Не существует прямого способа проверить, поддерживается ли функция, поэтому обнаружение функции основано на наблюдении за ее поведением. Таким образом, следующий пример основан на определении того, сохраняется ли тег <style> , что указывает на поддержку, или он является встроенным, что указывает на отсутствие поддержки. Обратите внимание: чтобы это работало, страница уже должна получить разрешение буфера обмена.

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

Демо

Чтобы увидеть unsanitized опцию в действии, посмотрите демо на Glitch и ознакомьтесь с ее исходным кодом .

Выводы

Как указано во введении, большинству разработчиков никогда не придется беспокоиться об очистке буфера обмена, и они могут просто работать с вариантами очистки по умолчанию, выбранными браузером. В тех редких случаях, когда разработчикам нужно позаботиться, существует вариант unsanitized .

Благодарности

Эту статью рецензировали Анупам Снигдха и Рэйчел Эндрю . API был указан и реализован командой Microsoft Edge.