Async Clipboard API 中的 HTML 未經處理

自 Chrome 120 版起,Async 剪貼簿 API 會提供新的 unsanitized 選項。這個選項可在 HTML 的特殊情況下派上用場,也就是您需要貼上剪貼簿中的內容,與複製時的方式完全相同。也就是說,如果沒有瀏覽器普遍且基於正當理由,而採取任何中繼清理步驟即可。本指南將說明如何使用。

在大多數情況下,使用 Async Clipboard API 時,開發人員不需要擔心剪貼簿內容的完整性,可以假設他們「寫入」剪貼簿 (複製) 的內容從剪貼簿「讀取」的資料 (貼上)。

這對於文字來說十分重要。請嘗試將下列程式碼貼到開發人員工具,然後立即重新聚焦頁面。(必須使用 setTimeout(),因此您有足夠的時間聚焦頁面,這是 Async 剪貼簿 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 來避免發生問題。請考慮以下範例,並在開發人員工具控制台中試用看看。您會發現輸出內容與輸入內容有很大的差異。

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

使用 Async Clipboard API 將 write() (複製) HTML 複製到剪貼簿時,瀏覽器會透過 DOM 剖析器執行並序列化產生的 HTML 字串,確保格式正確,但這個步驟不會進行掃毒。您不需要採取任何行動。當您 read() HTML 加到其他應用程式的剪貼簿中,且網頁應用程式選擇取得完整的擬真度內容,且需要在自己的程式碼中執行任何掃毒時,您可以透過屬性 unsanitized 和值為 ['text/html'] 將選項物件傳遞至 read() 方法。單獨看起來會像這樣:navigator.clipboard.read({ unsanitized: ['text/html'] })。下列程式碼範例與先前的程式碼幾乎相同,但這次使用 unsanitized 選項。在開發人員工具控制台中試用時,您會看到輸入和輸出內容相同。

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 選項。

特別銘謝

本文由 Anupam SnigdhaRachel Andrew 評論。這個 API 是由 Microsoft Edge 團隊指定及實作。