Nicht bereinigter HTML-Code in der Async Clipboard API

Ab Chrome 120 ist in der Async Clipboard API eine neue Option unsanitized verfügbar. Diese Option kann in bestimmten Situationen mit HTML hilfreich sein, in denen Sie den Inhalt der Zwischenablage genau so einfügen müssen, wie er beim Kopieren war. Das heißt, ohne einen Zwischenschritt zur Bereinigung, den Browser häufig – und aus guten Gründen – anwenden. In diesem Leitfaden erfahren Sie, wie Sie die Funktion verwenden.

Bei der Arbeit mit der Async Clipboard API müssen sich Entwickler in den meisten Fällen keine Gedanken über die Integrität der Inhalte in der Zwischenablage machen. Sie können davon ausgehen, dass die Daten, die sie in die Zwischenablage schreiben (kopieren), dieselben sind, die sie erhalten, wenn sie die Daten aus der Zwischenablage lesen (einfügen).

Das gilt definitiv für Text. Fügen Sie den folgenden Code in die DevTools-Konsole ein und lenken Sie den Fokus dann sofort auf die Seite. (Die setTimeout() ist erforderlich, damit Sie genügend Zeit haben, den Fokus auf die Seite zu legen. Dies ist eine Anforderung der Async Clipboard API.) Wie Sie sehen, ist die Eingabe genau mit der Ausgabe identisch.

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

Bei Bildern sieht das etwas anders aus. Um sogenannte Komprimierungsbomben zu verhindern, codieren Browser Bilder wie PNGs neu. Die Eingabe- und Ausgabebilder sind jedoch visuell pixelgenau identisch.

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

Was passiert jedoch mit HTML-Text? Wie Sie vielleicht vermutet haben, ist das bei HTML anders. Hier wird der HTML-Code vom Browser bereinigt, um unerwünschte Vorgänge zu verhindern. Dazu werden beispielsweise <script>-Tags aus dem HTML-Code entfernt (und andere wie <meta>, <head> und <style>) und CSS wird inline eingefügt. Sehen Sie sich das folgende Beispiel an und testen Sie es in der DevTools-Konsole. Sie werden feststellen, dass sich die Ausgabe deutlich von der Eingabe unterscheidet.

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-Bereinigung ist im Allgemeinen eine gute Sache. Sie sollten in den meisten Fällen kein nicht bereinigtes HTML zulassen, da dies Sicherheitsrisiken birgt. Es gibt jedoch Szenarien, in denen der Entwickler genau weiß, was er tut und in denen die Integrität der Eingabe- und Ausgabe-HTML für die ordnungsgemäße Funktion der App entscheidend ist. In diesen Fällen haben Sie zwei Möglichkeiten:

  1. Wenn Sie sowohl das Kopieren als auch das Einfügen steuern, z. B. wenn Sie etwas innerhalb Ihrer App kopieren und dann wieder in Ihrer App einfügen, sollten Sie benutzerdefinierte Webformate für die Async Clipboard API verwenden. Lesen Sie den verlinkten Artikel.
  2. Wenn Sie nur das Ende des Einfügens in Ihrer App festlegen, aber nicht das Ende des Kopiervorgangs, sollten Sie die Option unsanitized verwenden, die im Rest dieses Artikels erläutert wird. Das liegt möglicherweise daran, dass der Kopiervorgang in einer nativen App erfolgt, die keine benutzerdefinierten Webformate unterstützt.

Dazu gehören beispielsweise das Entfernen von script-Tags, das Einfügen von Stilen und das Sicherstellen, dass die HTML-Datei korrekt formatiert ist. Diese Liste ist nicht vollständig und es können in Zukunft weitere Schritte hinzugefügt werden.

Kopieren und Einfügen von nicht bereinigtem HTML

Wenn Sie mit der Async Clipboard API HTML in die Zwischenablage write() (kopieren), prüft der Browser, ob es korrekt formatiert ist, indem er es durch einen DOM-Parser führt und den resultierenden HTML-String serialisiert. In diesem Schritt wird jedoch keine Bereinigung durchgeführt. Sie müssen nichts weiter tun. Wenn Sie read()-HTML von einer anderen Anwendung in die Zwischenablage kopieren und Ihre Webanwendung die vollständigen Inhalte abrufen und in Ihrem eigenen Code bereinigen möchte, können Sie der read()-Methode ein Optionsobjekt mit dem Attribut unsanitized und dem Wert ['text/html'] übergeben. Für sich genommen sieht das so aus: navigator.clipboard.read({ unsanitized: ['text/html'] }). Das folgende Codebeispiel ist fast identisch mit dem vorherigen, enthält aber diesmal die Option unsanitized. Wenn Sie es in der DevTools-Konsole ausprobieren, sehen Sie, dass Eingabe und Ausgabe identisch sind.

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

Browserunterstützung und Funktionserkennung

Es gibt keine direkte Möglichkeit, zu prüfen, ob die Funktion unterstützt wird. Daher basiert die Funktionserkennung auf der Beobachtung des Verhaltens. Daher basiert das folgende Beispiel darauf, ob ein <style>-Tag bestehen bleibt, was auf Unterstützung hinweist, oder ob ein Inline-Tag vorhanden ist, was auf Unterstützung hinweist. Damit dies funktioniert, muss die Seite bereits die Berechtigung für die Zwischenablage erhalten haben.

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

Demo

Eine Demo der Option unsanitized findest du auf Glitch und den Quellcode dazu.

Schlussfolgerungen

Wie in der Einführung erläutert, müssen sich die meisten Entwickler nie um die Bereinigung des Zwischenspeichers kümmern und können einfach die Standardoptionen für die Bereinigung verwenden, die vom Browser festgelegt werden. Für die seltenen Fälle, in denen Entwickler sich darum kümmern müssen, gibt es die Option unsanitized.

Danksagungen

Dieser Artikel wurde von Anupam Snigdha und Rachel Andrew gelesen. Die API wurde vom Microsoft Edge-Team festgelegt und implementiert.