Odblokowuję dostęp do schowka

Bezpieczniejszy i odblokowany dostęp do schowka – tekst i obrazy

Tradycyjnym sposobem uzyskiwania dostępu do schowka systemowego było korzystanie ze schowka za pomocą document.execCommand(). Ta metoda wycinania i wklejania danych jest powszechnie obsługiwana, ale wiąże się to z kosztami: dostęp do schowka był synchroniczny, więc można było tylko odczytywać i zapisywać dane w DOM.

W przypadku małych fragmentów tekstu jest to dozwolone, ale w wielu przypadkach zablokowanie strony w celu przeniesienia danych do schowka może być niekorzystne. Bezpieczne wklejanie treści może wymagać czasochłonnego oczyszczania lub dekodowania obrazu. Być może przeglądarka będzie musiała wczytać lub umieścić w tekście połączone zasoby z wklejonego dokumentu. Spowoduje to zablokowanie strony podczas oczekiwania na dysk lub sieć. Wyobraź sobie, że dodajesz do tego zestawu uprawnienia, które wymagają, aby przeglądarka blokowała stronę podczas wysyłania żądania dostępu do schowka. Jednocześnie uprawnienia przyznane wokół document.execCommand() w celu interakcji ze schowka są luźno zdefiniowane i różnią się w zależności od przeglądarki.

Interfejs Async Clipboard API rozwiązuje te problemy i zapewnia zdefiniowany model uprawnień, który nie blokuje strony. Interfejs Async Clipboard API może obsługiwać tylko tekst i obrazy w większości przeglądarek, ale jego obsługa jest różna. Zapoznaj się dokładnie z omówieniem zgodności przeglądarek w poniższych sekcjach.

Kopiowanie: zapisywanie danych w schowku

writeText()

Aby skopiować tekst do schowka, wywołaj writeText(). Ten interfejs API jest asynchroniczny, więc funkcja writeText() zwraca obietnicę, która rozwiązuje lub odrzuca prośbę w zależności od tego, czy przekazany tekst został skopiowany:

async function copyPageUrl() {
  try {
    await navigator.clipboard.writeText(location.href);
    console.log('Page URL copied to clipboard');
  } catch (err) {
    console.error('Failed to copy: ', err);
  }
}

Obsługa przeglądarek

  • 66
  • 79
  • 63
  • 13.1

Źródło

zapis()

W rzeczywistości writeText() to wygodna metoda w przypadku ogólnej metody write(), która także umożliwia kopiowanie obrazów do schowka. Podobnie jak writeText(), działa asynchronicznie i zwraca obietnicę.

Aby zapisać obraz w schowku, musisz użyć go jako blob. Możesz to zrobić na przykład przez wysłanie żądania udostępnienia obrazu do serwera za pomocą funkcji fetch(), a następnie wywołanie metody blob() w odpowiedzi.

Żądanie zdjęcia z serwera może być niepożądane lub możliwe z różnych powodów. Na szczęście możesz narysować obraz na płótnie i wywołać metodę toBlob() obiektu canvas.

Następnie przekaż tablicę obiektów ClipboardItem jako parametr do metody write(). Obecnie możesz przesyłać tylko 1 obraz naraz, ale w przyszłości planujemy dodać obsługę większej liczby obrazów. ClipboardItem przyjmuje jako klucz obiekt z typem MIME obrazu, a wartością – obiekt blob. W przypadku obiektów blob uzyskanych z fetch() lub canvas.toBlob() właściwość blob.type automatycznie zawiera prawidłowy typ MIME obrazu.

try {
  const imgURL = '/images/generic/file.png';
  const data = await fetch(imgURL);
  const blob = await data.blob();
  await navigator.clipboard.write([
    new ClipboardItem({
      // The key is determined dynamically based on the blob's type.
      [blob.type]: blob
    })
  ]);
  console.log('Image copied.');
} catch (err) {
  console.error(err.name, err.message);
}

Możesz też napisać obietnicę do obiektu ClipboardItem. W przypadku tego wzorca musisz wcześniej znać typ MIME danych.

try {
  const imgURL = '/images/generic/file.png';
  await navigator.clipboard.write([
    new ClipboardItem({
      // Set the key beforehand and write a promise as the value.
      'image/png': fetch(imgURL).then(response => response.blob()),
    })
  ]);
  console.log('Image copied.');
} catch (err) {
  console.error(err.name, err.message);
}

Obsługa przeglądarek

  • 66
  • 79
  • 13.1

Źródło

Kopiowanie zdarzenia

Jeśli użytkownik inicjuje kopiowanie schowka i nie wywołuje funkcji preventDefault(), zdarzenie copy zawiera właściwość clipboardData z elementami już we właściwym formacie. Jeśli chcesz zastosować własną logikę, musisz wywołać metodę preventDefault(), aby zapobiec domyślnemu działaniu na rzecz własnej implementacji. W tym przypadku pole clipboardData jest puste. Weź pod uwagę stronę z tekstem i obrazem, a gdy użytkownik wybierze wszystko i zainicjuje kopiowanie do schowka, Twoje niestandardowe rozwiązanie powinno odrzucić tekst i skopiować tylko obraz. Możesz to zrobić, jak pokazano w poniższym przykładzie kodu. Nie opisujemy w tym przykładzie, jak wrócić do wcześniejszych interfejsów API, gdy interfejs Clipboard API nie jest obsługiwany.

<!-- The image we want on the clipboard. -->
<img src="kitten.webp" alt="Cute kitten.">
<!-- Some text we're not interested in. -->
<p>Lorem ipsum</p>
document.addEventListener("copy", async (e) => {
  // Prevent the default behavior.
  e.preventDefault();
  try {
    // Prepare an array for the clipboard items.
    let clipboardItems = [];
    // Assume `blob` is the blob representation of `kitten.webp`.
    clipboardItems.push(
      new ClipboardItem({
        [blob.type]: blob,
      })
    );
    await navigator.clipboard.write(clipboardItems);
    console.log("Image copied, text ignored.");
  } catch (err) {
    console.error(err.name, err.message);
  }
});

Dla wydarzenia copy:

Obsługa przeglądarek

  • 1
  • 12
  • 22
  • 3

Źródło

ClipboardItem:

Obsługa przeglądarek

  • 76
  • 79
  • 13.1

Źródło

Wklej: odczyt danych ze schowka

readText()

Aby odczytać tekst ze schowka, wywołaj funkcję navigator.clipboard.readText() i poczekaj na rozwiązanie zwróconej obietnicy:

async function getClipboardContents() {
  try {
    const text = await navigator.clipboard.readText();
    console.log('Pasted content: ', text);
  } catch (err) {
    console.error('Failed to read clipboard contents: ', err);
  }
}

Obsługa przeglądarek

  • 66
  • 79
  • 13.1

Źródło

Read()

Metoda navigator.clipboard.read() też jest asynchroniczna i zwraca obietnicę. Aby odczytać obraz ze schowka, uzyskaj listę obiektów ClipboardItem i powtórz je.

Każdy obiekt ClipboardItem może zawierać swoją zawartość w różnych typach, więc musisz iterować listę typów, używając pętli for...of. Dla każdego typu wywołaj metodę getType() z bieżącym typem jako argumentem, aby uzyskać odpowiedni obiekt blob. Tak jak poprzednio, ten kod nie jest powiązany z obrazami i będzie działał z innymi typami plików w przyszłości.

async function getClipboardContents() {
  try {
    const clipboardItems = await navigator.clipboard.read();
    for (const clipboardItem of clipboardItems) {
      for (const type of clipboardItem.types) {
        const blob = await clipboardItem.getType(type);
        console.log(URL.createObjectURL(blob));
      }
    }
  } catch (err) {
    console.error(err.name, err.message);
  }
}

Obsługa przeglądarek

  • 66
  • 79
  • 13.1

Źródło

Praca z wklejonymi plikami

Użytkownicy mogą korzystać ze schowka za pomocą skrótów klawiszowych, takich jak Ctrl + C lub Ctrl + V. Chromium ujawnia w schowku pliki tylko do odczytu, jak opisano poniżej. Ten komunikat jest uruchamiany, gdy użytkownik kliknie domyślny skrót wklejania w systemie operacyjnym lub kliknie Edytuj, a następnie Wklej na pasku menu przeglądarki. Nie musisz dodawać żadnego kodu hydraulicznego.

document.addEventListener("paste", async e => {
  e.preventDefault();
  if (!e.clipboardData.files.length) {
    return;
  }
  const file = e.clipboardData.files[0];
  // Read the file's contents, assuming it's a text file.
  // There is no way to write back to it.
  console.log(await file.text());
});

Obsługa przeglądarek

  • 3
  • 12
  • 3.6
  • 4

Źródło

Zdarzenie wklejenia

Jak już wspomnieliśmy, planujemy wprowadzić zdarzenia, które będą obsługiwać interfejs Clipboard API, ale na razie możesz korzystać z istniejącego zdarzenia paste. Świetnie współpracuje z nowymi metodami odczytywania tekstu ze schowka. Tak jak w przypadku zdarzenia copy, nie zapomnij wywołać preventDefault().

document.addEventListener('paste', async (e) => {
  e.preventDefault();
  const text = await navigator.clipboard.readText();
  console.log('Pasted text: ', text);
});

Obsługa przeglądarek

  • 1
  • 12
  • 22
  • 3

Źródło

Obsługa wielu typów MIME

Większość implementacji umieszcza w schowku wiele formatów danych na potrzeby pojedynczej operacji wycinania lub kopiowania. Istnieją 2 powody: deweloper nie może poznać możliwości aplikacji, do której chce skopiować tekst lub obrazy, a wiele aplikacji obsługuje wklejanie uporządkowanych danych jako zwykłego tekstu. Zwykle wyświetla się użytkownikom, którzy mają pozycję menu Edytuj o nazwie Wklej i dopasuj styl lub Wklej bez formatowania.

Poniższy przykład pokazuje, jak to zrobić. W tym przykładzie do uzyskiwania danych obrazu jest używany znacznik fetch(), ale może on też pochodzić z interfejsu <canvas> lub File System Access API.

async function copy() {
  const image = await fetch('kitten.png').then(response => response.blob());
  const text = new Blob(['Cute sleeping kitten'], {type: 'text/plain'});
  const item = new ClipboardItem({
    'text/plain': text,
    'image/png': image
  });
  await navigator.clipboard.write([item]);
}

Zabezpieczenia i uprawnienia

Dostęp do schowka zawsze stanowił zagrożenie dla bezpieczeństwa przeglądarek. Bez odpowiednich uprawnień strona mogłaby dyskretnie kopiować do schowka użytkownika wszelkie szkodliwe treści, co po ich wklejeniu spowodowałoby katastrofalne skutki. Wyobraź sobie stronę internetową, która dyskretnie kopiuje do schowka plik rm -rf / lub obraz bomby dekompresyjnej.

Prompt przeglądarki z prośbą o przyznanie uprawnień dostępu do schowka.
Prośba o uprawnienia dotycząca interfejsu Clipboard API.

Zapewnianie stronom internetowym nieograniczonego dostępu do schowka, na którym można odczytywać dane, jest jeszcze trudniejsze. Użytkownicy rutynowo kopiują do schowka informacje poufne, takie jak hasła i dane osobowe, które mogą zostać odczytane przez dowolną stronę bez ich wiedzy.

Podobnie jak wiele nowych interfejsów API, interfejs Clipboard API jest obsługiwany tylko na stronach wyświetlanych przez HTTPS. Aby zapobiec nadużyciom, dostęp do schowka jest dozwolony tylko wtedy, gdy strona jest aktywną kartą. Strony w aktywnych kartach mogą zapisywać dane w schowku bez pytania o uprawnienia, ale odczyt ze schowka zawsze wymaga odpowiednich uprawnień.

Uprawnienia do kopiowania i wklejania zostały dodane do interfejsu Permissions API. Uprawnienie clipboard-write jest przyznawane automatycznie stronom, gdy są one aktywną kartą. Wymagane jest uprawnienie clipboard-read. Możesz je odczytać ze schowka. Poniższy kod przedstawia ten ostatni kod:

const queryOpts = { name: 'clipboard-read', allowWithoutGesture: false };
const permissionStatus = await navigator.permissions.query(queryOpts);
// Will be 'granted', 'denied' or 'prompt':
console.log(permissionStatus.state);

// Listen for changes to the permission state
permissionStatus.onchange = () => {
  console.log(permissionStatus.state);
};

Za pomocą opcji allowWithoutGesture możesz też określić, czy do wycinania lub wklejania jest wymagany gest użytkownika. Ta wartość różni się w zależności od przeglądarki, dlatego należy ją zawsze uwzględniać.

W tym przypadku naprawdę przydaje się asynchroniczna natura interfejsu Clipboard API: przy próbie odczytu lub zapisu danych ze schowka automatycznie wyświetli się prośba o udzielenie użytkownikowi uprawnień, jeśli nie zostały one jeszcze przyznane. Interfejs API opiera się na obietnicach, więc zasady te są całkowicie przejrzyste, a użytkownik odmawiający przyznania uprawnień do korzystania z schowka obietnica odrzuca obietnicę, więc strona może odpowiednio zareagować.

Przeglądarki zezwalają na dostęp do schowka tylko wtedy, gdy strona jest aktywną kartą, więc niektóre z podanych tu przykładów nie działają po wklejeniu bezpośrednio w konsoli przeglądarki, ponieważ same narzędzia dla programistów są aktywną kartą. Oto sztuczka: odłóż dostęp do schowka za pomocą elementu setTimeout(), a następnie szybko kliknij w środku strony, aby zaznaczyć ją przed wywołaniem funkcji:

setTimeout(async () => {
  const text = await navigator.clipboard.readText();
  console.log(text);
}, 2000);

Integracja zasad uprawnień

Aby używać interfejsu API w elementach iframe, musisz włączyć go za pomocą zasady uprawnień, która określa mechanizm, który umożliwia selektywne włączanie i wyłączanie różnych funkcji przeglądarek i interfejsów API. W zależności od potrzeb aplikacji musisz zaliczyć jeden lub oba punkty clipboard-read lub clipboard-write.

<iframe
    src="index.html"
    allow="clipboard-read; clipboard-write"
>
</iframe>

Wykrywanie funkcji

Aby korzystać z interfejsu Async Clipboard API podczas obsługi wszystkich przeglądarek, przetestuj navigator.clipboard i wróć do wcześniejszych metod. Oto, jak można zastosować wklejanie, aby uwzględnić inne przeglądarki.

document.addEventListener('paste', async (e) => {
  e.preventDefault();
  let text;
  if (navigator.clipboard) {
    text = await navigator.clipboard.readText();
  }
  else {
    text = e.clipboardData.getData('text/plain');
  }
  console.log('Got pasted text: ', text);
});

To nie wszystko. Przed wprowadzeniem interfejsu Async Clipboard API można było korzystać z różnych implementacji kopiowania i wklejania w różnych przeglądarkach. W większości przeglądarek własne kopiowanie i wklejanie można aktywować za pomocą funkcji document.execCommand('copy') i document.execCommand('paste'). Jeśli tekst do skopiowania to ciąg znaków niedostępny w DOM, należy go wstrzyknąć do elementu DOM i zaznaczyć:

button.addEventListener('click', (e) => {
  const input = document.createElement('input');
  input.style.display = 'none';
  document.body.appendChild(input);
  input.value = text;
  input.focus();
  input.select();
  const result = document.execCommand('copy');
  if (result === 'unsuccessful') {
    console.error('Failed to copy text.');
  }
  input.remove();
});

Przykłady

Możesz pobawić się interfejsem Async Clipboard API w poniższych wersjach demonstracyjnych. W Glitch możesz zremiksować prezentację tekstową lub prezentację obrazu, aby z nimi poeksperymentować.

Pierwszy przykład przedstawia przenoszenie tekstu w schowku i poza nim.

Aby wypróbować interfejs API z obrazami, skorzystaj z tej wersji demonstracyjnej. Pamiętaj, że obsługiwane są tylko pliki PNG i tylko w niektórych przeglądarkach.

Podziękowania

Interfejs Asynchronous Clipboard API został wdrożony przez Darwina Huanga i Gary'ego Kačmarčíka. Darwin również dostarczył demonstrację. Dziękujemy Kiarikowi i jeszcze raz Gary'emu Kačmarčíkowi za zapoznanie się z częścią tego artykułu.

Baner powitalny autorstwa Markusa Winklera w serwisie Unsplash.