HTML chưa được dọn dẹp trong API Bảng nhớ tạm không đồng bộ

Kể từ Chrome 120, một tuỳ chọn unsanitized mới sẽ có trong API Bảng nhớ tạm không đồng bộ. Tuỳ chọn này có thể hữu ích trong các trường hợp đặc biệt với HTML, khi bạn cần dán nội dung của bảng nhớ tạm giống như khi nội dung đó được sao chép. Tức là không có bước dọn dẹp trung gian nào mà trình duyệt thường áp dụng (và vì lý do chính đáng). Tìm hiểu cách sử dụng tính năng này trong hướng dẫn này.

Khi làm việc với Async Clipboard API, trong hầu hết các trường hợp, nhà phát triển không cần phải lo lắng về tính toàn vẹn của nội dung trên bảng nhớ tạm và có thể giả định rằng nội dung họ ghi vào bảng nhớ tạm (sao chép) giống với nội dung họ sẽ nhận được khi đọc dữ liệu từ bảng nhớ tạm (dán).

Điều này chắc chắn đúng với văn bản. Hãy thử dán mã sau vào Bảng điều khiển công cụ cho nhà phát triển rồi lấy lại tiêu điểm trang ngay lập tức. (setTimeout() là cần thiết để bạn có đủ thời gian lấy tiêu điểm trang. Đây là yêu cầu của API bảng nhớ tạm Async.) Như bạn thấy, dữ liệu đầu vào chính xác giống với dữ liệu đầu ra.

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

Với hình ảnh, quy trình sẽ khác một chút. Để ngăn chặn các cuộc tấn công được gọi là bom nén, trình duyệt sẽ mã hoá lại các hình ảnh như PNG, nhưng hình ảnh đầu vào và đầu ra về mặt hình ảnh giống hệt nhau, từng 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);

Tuy nhiên, điều gì sẽ xảy ra với văn bản HTML? Như bạn có thể đoán, với HTML, tình huống sẽ khác. Tại đây, trình duyệt sẽ dọn dẹp mã HTML để ngăn chặn các sự cố, chẳng hạn như xoá các thẻ <script> khỏi mã HTML (và các thẻ khác như <meta>, <head><style>) và bằng cách nội tuyến CSS. Hãy xem ví dụ sau và thử trong Bảng điều khiển DevTools. Bạn sẽ nhận thấy kết quả đầu ra khác biệt khá đáng kể so với dữ liệu đầu vào.

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

Việc dọn dẹp HTML thường là một việc tốt. Bạn không nên tiếp xúc với các vấn đề bảo mật khi cho phép HTML chưa được dọn dẹp trong phần lớn các trường hợp. Tuy nhiên, có những trường hợp nhà phát triển biết chính xác những gì họ đang làm và tính toàn vẹn của HTML đầu vào và đầu ra là yếu tố quan trọng để ứng dụng hoạt động chính xác. Trong những trường hợp này, bạn có hai lựa chọn:

  1. Nếu bạn kiểm soát cả quá trình sao chép và dán, ví dụ: nếu bạn sao chép từ trong ứng dụng rồi dán tương tự trong ứng dụng, bạn nên sử dụng các định dạng tuỳ chỉnh trên web cho Async Clipboard API. Dừng đọc tại đây và xem bài viết được liên kết.
  2. Nếu chỉ kiểm soát phần dán trong ứng dụng nhưng không kiểm soát phần sao chép, có thể là do thao tác sao chép diễn ra trong một ứng dụng gốc không hỗ trợ các định dạng tuỳ chỉnh trên web, bạn nên sử dụng tuỳ chọn unsanitized được giải thích trong phần còn lại của bài viết này.

Quá trình dọn dẹp bao gồm các thao tác như xoá thẻ script, nội tuyến kiểu và đảm bảo HTML có định dạng hợp lệ. Danh sách này không đầy đủ và có thể sẽ thêm các bước khác trong tương lai.

Sao chép và dán HTML chưa được dọn dẹp

Khi bạn write() (sao chép) HTML vào bảng nhớ tạm bằng Async Clipboard API, trình duyệt sẽ đảm bảo rằng HTML được định dạng đúng cách bằng cách chạy HTML đó thông qua trình phân tích cú pháp DOM và chuyển đổi tuần tự chuỗi HTML thu được, nhưng không có quá trình dọn dẹp nào diễn ra ở bước này. Bạn không cần làm gì cả. Khi một ứng dụng khác đặt read() HTML vào bảng nhớ tạm và ứng dụng web của bạn chọn nhận nội dung có độ trung thực đầy đủ và cần thực hiện bất kỳ hành động dọn dẹp nào trong mã của riêng bạn, bạn có thể truyền một đối tượng tuỳ chọn đến phương thức read() bằng thuộc tính unsanitized và giá trị ['text/html']. Khi tách riêng, mã này sẽ có dạng như sau: navigator.clipboard.read({ unsanitized: ['text/html'] }). Mã mẫu bên dưới gần giống với mã mẫu trước đó, nhưng lần này với lựa chọn unsanitized. Khi thử trong bảng điều khiển DevTools, bạn sẽ thấy dữ liệu đầu vào và đầu ra giống nhau.

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

Hỗ trợ trình duyệt và phát hiện tính năng

Không có cách trực tiếp để kiểm tra xem tính năng này có được hỗ trợ hay không. Vì vậy, việc phát hiện tính năng dựa trên việc quan sát hành vi đó. Do đó, ví dụ sau đây dựa vào việc phát hiện thực tế liệu thẻ <style> có tồn tại hay không, cho biết tính năng hỗ trợ hoặc đang được nội tuyến, cho biết tính năng không hỗ trợ. Xin lưu ý rằng để tính năng này hoạt động, trang cần phải có quyền truy cập vào bảng nhớ tạm.

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

Bản minh hoạ

Để xem tuỳ chọn unsanitized hoạt động như thế nào, hãy xem bản minh hoạ trên Glitch và tham khảo mã nguồn của bản minh hoạ đó.

Kết luận

Như đã nêu trong phần giới thiệu, hầu hết các nhà phát triển sẽ không bao giờ phải lo lắng về việc dọn dẹp bảng nhớ tạm và chỉ có thể làm việc với các lựa chọn dọn dẹp mặc định do trình duyệt thực hiện. Trong một số ít trường hợp mà nhà phát triển cần quan tâm, tuỳ chọn unsanitized sẽ tồn tại.

Lời cảm ơn

Bài viết này đã được Anupam SnigdhaRachel Andrew xem xét. API này do nhóm Microsoft Edge chỉ định và triển khai.