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

Trên Chrome 120, một tuỳ chọn unsanitized mới đã 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, trong đó bạn cần dán nội dung của bảng nhớ tạm giống với nội dung khi được sao chép. Tức là không có bước dọn dẹp trung gian nào mà các trình duyệt thường áp dụng (vì lý do chính đáng). Hãy tìm hiểu cách sử dụng công cụ này trong hướng dẫn này.

Khi làm việc với Async clipboard API, trong hầu hết trường hợp, nhà phát triển không cần 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 (bản sao) giống với những gì 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 cho nhà phát triển rồi lấy nét lại trang ngay lập tức. (setTimeout() cần thiết để bạn có đủ thời gian tập trung vào trang, đây là yêu cầu của Async Clipboard API.) 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);

Có hình ảnh hơi khác một chút. Để ngăn chặn các cuộc tấn công gọi là bom nén, trình duyệt sẽ mã hoá lại hình ảnh như PNG nhưng hình ảnh đầu vào và hình ảnh đầu ra trực quan giống hệt nhau, pixel trên mỗi 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ì xảy ra với văn bản HTML? Như bạn có thể đoán, với HTML, tình hình sẽ khác. Tại đây, trình duyệt sẽ dọn dẹp mã HTML để ngăn chặn điều xấu xảy ra, chẳng hạn như bằng cách tách thẻ <script> khỏi mã HTML (và các thẻ khác như <meta>, <head><style>) và bằng cách cùng dòng CSS. Hãy xem xét ví dụ sau và thử áp dụng trong Bảng điều khiển công cụ cho nhà phát triển. Bạn sẽ nhận thấy rằng kết quả 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);

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

  1. Ví dụ: nếu bạn kiểm soát cả thao tác sao chép và cuối dán, chẳng hạn như nếu bạn sao chép từ trong ứng dụng để sau đó dán trong ứng dụng của mình, bạn nên sử dụng Định dạng tuỳ chỉnh trên web cho API Bảng nhớ tạm không đồng bộ. Vui lòng dừng đọc ở đây và xem bài viết được liên kết.
  2. Nếu bạn chỉ kiểm soát phần cuối của thao tác dán trong ứng dụng mà không kiểm soát phần cuối của việc sao chép, thì 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, thì 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.

Quy trình dọn dẹp bao gồm những hoạt động như xoá thẻ script, kiểu cùng dòng và đảm bảo HTML được định dạng đúng. Danh sách này không đầy đủ và chúng tôi có thể 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 SafeFrame API (API Bảng nhớ tạm không đồng bộ), trình duyệt sẽ đảm bảo định dạng đúng bằng cách chạy mã này thông qua trình phân tích cú pháp DOM và chuyển đổi tuần tự chuỗi HTML kết quả, nhưng không có quá trình dọn dẹp nào đang 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 HTML read() trên bảng nhớ tạm, ứng dụng web của bạn đang chọn nhận nội dung chân thực đầy đủ và cần thực hiện mọi quy trình dọn dẹp trong mã của riêng mình, bạn có thể truyền một đối tượng tuỳ chọn đến phương thức read() có thuộc tính unsanitized và giá trị ['text/html']. Cụ thể, giao diện sẽ có dạng như sau: navigator.clipboard.read({ unsanitized: ['text/html'] }). Mã mẫu sau đây ở bên dưới gần giống với mã mẫu hiển thị trước đó, nhưng lần này với tuỳ chọn unsanitized. Khi thử dùng trong Bảng điều khiển công cụ cho nhà phát triển, bạn sẽ thấy dữ liệu đầu vào và dữ liệu đầ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 có được hỗ trợ hay không. Vì vậy, việc phát hiện tính năng sẽ dựa trên quan sát hành vi. Do đó, ví dụ sau dựa vào việc phát hiện thực tế là thẻ <style> còn tồn tại hay không, điều này có nghĩa là thẻ được hỗ trợ hay đang nằm cùng dòng, tức là thẻ 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 cách hoạt động của tuỳ chọn unsanitized, hãy xem bản minh hoạ về sự cố nhiễu và xem mã nguồn của tuỳ chọn đó.

Kết luận

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

Xác nhận

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