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

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

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

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

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. Если вы контролируете и копирование, и вставку, например, если вы копируете из своего приложения, чтобы затем вставить в свое приложение, вам следует использовать пользовательские веб-форматы для API асинхронного буфера обмена . Прекратите читать здесь и проверьте связанную статью.
  2. Если вы контролируете только окончание вставки в своем приложении, но не окончание копирования, возможно, потому, что операция копирования происходит в собственном приложении, которое не поддерживает пользовательские веб-форматы, вам следует использовать опцию unsanitized , которая объясняется в оставшейся части этой статьи.

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

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

Когда вы write() (копируете) HTML в буфер обмена с помощью API Async Clipboard, браузер проверяет, что он правильно сформирован, пропуская его через парсер DOM и сериализуя полученную строку HTML, но на этом этапе очистка не выполняется. Вам ничего не нужно делать. Когда вы read() HTML, помещенный в буфер обмена другим приложением, и ваше веб-приложение выбирает получение контента полной точности и нуждается в выполнении какой-либо очистки в вашем собственном коде, вы можете передать объект options методу 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 опцию в действии, посмотрите демо-версию и ознакомьтесь с ее исходным кодом .

Выводы

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

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

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