Async Clipboard API 中的 HTML 未經處理

從 Chrome 120 開始,Async Clipboard API 中提供新的 unsanitized 選項。這個選項可在使用 HTML 時有所幫助,因為您必須按照複製時的方式貼上剪貼簿的內容。也就是說,沒有任何中繼的清理步驟,而這正是瀏覽器常見的做法,也是有其原因的。請參閱本指南,瞭解如何使用這項功能。

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

文字也是如此。請在開發人員工具控制台中貼上下列程式碼,然後立即重新對焦頁面。(setTimeout() 是 Async Clipboard 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 程式碼,以防發生不良事件,例如從 HTML 程式碼 (以及 <meta><head><style> 等其他程式碼) 中移除 <script> 標記,並內嵌 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 剪貼簿 API 的 Web 自訂格式。請停止閱讀這裡的內容,並查看連結的文章。
  2. 如果您只控制應用程式中的貼上作業,而不能控制複製作業,可能是因為複製作業是在不支援網頁自訂格式的原生應用程式中執行,因此您應該使用 unsanitized 選項,我們會在本文的其餘部分加以說明。

包括移除 script 標記、內嵌樣式,以及確保 HTML 格式正確等。這份清單並非完整,日後可能會新增更多步驟。

複製及貼上未經過淨化的 HTML

當您使用 Async Clipboard API 將 HTML write() (複製) 到剪貼簿時,瀏覽器會透過 DOM 剖析器執行 HTML,並序列化產生的 HTML 字串,確保 HTML 格式正確,但不會在這個步驟中進行消毒。您不需要採取任何行動。當您 read() 由其他應用程式放置在剪貼簿上的 HTML,且您的網頁應用程式選擇取得完整保真度內容,並需要在自己的程式碼中執行任何消毒作業,您可以將選項物件傳遞至 read() 方法,其中包含屬性 unsanitized['text/html'] 值。單獨看起來如下所示: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 團隊指定及實作。