Odblokowuję dostęp do schowka

Bezpieczniejszy, nieblokowany dostęp do schowka dla tekstu i obrazów

Tradycyjny sposób uzyskiwania dostępu do schowka systemowego polegał na korzystaniu z document.execCommand()w przypadku interakcji ze schowkiem. Ta metoda wycinania i wklejania była szeroko obsługiwana, ale wiązała się z kosztami: dostęp do schowka był synchroniczny i można było tylko odczytywać oraz zapisywać dane w DOM.

Jest to dobre rozwiązanie w przypadku małych fragmentów tekstu, ale w wielu przypadkach zablokowanie strony w celu przeniesienia schowka nie jest przyjemne. Zanim będzie można bezpiecznie wkleić treści, może być konieczne ich ręczne oczyszczanie lub dekodowanie obrazu. Być może przeglądarka będzie musiała wczytać lub wbudować linki do zasobów z wklejonego dokumentu. To blokowałoby stronę podczas oczekiwania na dysk lub sieć. Wyobraź sobie, że masz w zestawie uprawnienia, które wymagają, aby przeglądarka blokowała stronę i prosiła o dostęp do schowka. Jednocześnie uprawnienia dotyczące interakcji z obsługą schowka są luźno zdefiniowane i różnią się w zależności od przeglądarki.

Async Clipboard API rozwiązuje te problemy, oferując dobrze zdefiniowany model uprawnień, który nie blokuje strony. Interfejs Async Clipboard API jest ograniczony do obsługi tekstu i obrazów w większości przeglądarek, ale obsługa może się różnić. W poszczególnych sekcjach dokładnie zapoznaj się z informacjami o zgodnności z przeglądarkami.

Kopiowanie: zapisywanie danych na schowku

writeText()

Aby skopiować tekst do schowka, naciśnij writeText(). Ten interfejs API jest asynchroniczny, więc funkcja writeText() zwraca obietnicę, która zostanie zrealizowana lub odrzucona w zależności od tego, czy przekazywany 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

  • Chrome: 66.
  • Edge: 79.
  • Firefox: 63.
  • Safari: 13.1.

Źródło

write()

W rzeczywistości writeText() to tylko wygodna metoda dla ogólnej metody write(), która umożliwia również kopiowanie obrazów do schowka. Podobnie jak writeText(), jest asynchroniczny i zwraca obietnicę.

Aby zapisać obraz w schowku, musisz mieć obraz w formacie blob. Jednym ze sposobów jest wysłanie żądania obrazu z serwera za pomocą fetch(), a potem wywołanie blob() w odpowiedzi.

Wysyłanie prośby o dostęp do obrazu z serwera może być niepożądane lub możliwe z różnych powodów. Na szczęście możesz też narysować obraz na płótnie i wywołać metodę płótna toBlob().

Następnie jako parametr metody write() prześlij tablicę obiektów ClipboardItem. Obecnie można przesłać tylko 1 obraz naraz, ale mamy nadzieję, że w przyszłości będziemy mogli dodać obsługę większej liczby obrazów. ClipboardItem przyjmuje obiekt z typem MIME obrazu jako klucz i blobem jako wartością. W przypadku obiektów blob uzyskanych z fetch() lub canvas.toBlob() właściwość blob.type automatycznie zawiera poprawny 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

  • Chrome: 76.
  • Edge: 79.
  • Firefox: 127.
  • Safari: 13.1

Źródło

Zdarzenie kopiowania

Jeśli użytkownik zainicjuje kopiowanie ze schowka, ale nie wywoła preventDefault(), zdarzenie copy będzie zawierać właściwość clipboardData z elementami w odpowiednim formacie. Jeśli chcesz zastosować własną logikę, musisz wywołać funkcję preventDefault(), aby zastąpić domyślne działanie własnym. W tym przypadku clipboardData będzie pusty. Załóżmy, że mamy stronę z tekstem i obrazem. Gdy użytkownik zaznaczy wszystko i zainicjuje kopię do schowka, niestandardowe rozwiązanie powinno odrzucić tekst i skopiować tylko obraz. Aby to zrobić, użyj przykładowego kodu poniżej. W tym przykładzie nie omawiamy sposobu korzystania z starszych 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);
  }
});

W przypadku wydarzenia copy:

Obsługa przeglądarek

  • Chrome: 1.
  • Krawędź: 12.
  • Firefox: 22.
  • Safari: 3.

Źródło

Do witryn ClipboardItem:

Obsługa przeglądarek

  • Chrome: 76.
  • Edge: 79.
  • Firefox: 127.
  • Safari: 13.1.

Źródło

Wklej: odczytywanie danych ze schowka

readText()

Aby odczytać tekst ze schowka, zadzwoń pod numer navigator.clipboard.readText() i poczekaj, aż zwrócona obietnica zostanie rozwiązana:

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

  • Chrome: 66.
  • Edge: 79.
  • Firefox: 125.
  • Safari: 13.1.

Źródło

Read()

Metoda navigator.clipboard.read() jest też asynchroniczna i zwraca obietnicę. Aby odczytać obraz ze schowka, pobierz listę obiektów ClipboardItem, a następnie przejdź przez nią.

Każdy element ClipboardItem może przechowywać różne typy treści, dlatego konieczne będzie powtórzenie ich listy ponownie za pomocą pętli for...of. W przypadku każdego typu wywołaj metodę getType() z bieżącym typem jako argumentem, aby uzyskać odpowiedni obiekt blob. Tak jak wcześniej, ten kod nie jest powiązany z obrazami i będzie działał z innymi typami plików, które zostaną utworzone 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

  • Chrome: 76.
  • Edge: 79.
  • Firefox: 127.
  • Safari: 13.1.

Źródło

Praca z wklejonymi plikami

Użytkownicy mogą korzystać ze skrótów klawiszowych w schowku, takich jak Ctrl + CCtrl + V. Chromium udostępnia pliki tylko do odczytu w schowku, jak opisano poniżej. Jest to wywoływane, gdy użytkownik kliknie domyślny skrót do wklejania w systemie operacyjnym lub gdy kliknie Edytuj, a potem Wklej na pasku menu przeglądarki. Nie musisz pisać dodatkowego kodu.

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

  • Chrome:
  • Krawędź: 12.
  • Firefox: 3.6
  • Safari: 4.

Źródło

Zdarzenie wklejania

Jak już wspomnieliśmy, planujemy wprowadzić zdarzenia do pracy z interfejsem Clipboard API, ale na razie możesz używać obecnego zdarzenia paste. Dobrze współpracuje z nowymi asynchronicznymi metodami odczytu tekstu z Schowka. Tak jak w przypadku zdarzenia copy, nie zapomnij wywołać funkcji preventDefault().

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

Obsługa przeglądarek

  • Chrome: 1.
  • Edge: 12.
  • Firefox: 22.
  • Safari: 3.

Źródło

Obsługa wielu typów MIME

Większość implementacji umieszcza wiele formatów danych na schowku w ramach pojedynczej operacji wycinania lub kopiowania. Jest to spowodowane 2 powodami: jako deweloper aplikacji nie masz możliwości poznania możliwości aplikacji, do której użytkownik chce skopiować tekst lub obrazy, a wiele aplikacji obsługuje wklejanie danych uporządkowanych jako zwykły tekst. Zazwyczaj jest to opcja w menu Edytuj o nazwie Wklej i dopasuj styl lub Wklej bez formatowania.

Poniżej znajdziesz przykład, jak to zrobić. W tym przykładzie dane obrazu są pobierane za pomocą interfejsu fetch(), ale mogą też pochodzić z interfejsu <canvas> lub File System 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 przeglądarek. Bez odpowiednich uprawnień strona może w sposób cichy kopiować wszelkiego rodzaju złośliwe treści do schowka użytkownika, co może spowodować katastrofalne skutki po wklejeniu. Wyobraź sobie stronę internetową, która po cichu kopiuje rm -rf / lub obraz z bombą dekompresyjną do schowka.

Prośba przeglądarki o przyznanie uprawnień do schowka
Prośba o uprawnienia dla interfejsu Clipboard API.

Przyznanie stronom internetowym nieograniczonego dostępu do schowka jest jeszcze bardziej kłopotliwe. Użytkownicy często kopiują na pulpit informacje poufne, takie jak hasła i dane osobowe, które mogą być odczytane przez dowolną stronę bez wiedzy użytkownika.

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

Do interfejsu API uprawnień dodano uprawnienia do kopiowania i wklejania. Uprawnienie clipboard-write jest przyznawane automatycznie stronom, gdy są one aktywną kartą. Musisz poprosić o uprawnienie clipboard-read – można je odczytać ze schowka. Poniżej przedstawiono kod, który pokazuje tę drugą opcję:

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

Możesz też określić, czy do wycinania lub wklejania wymagany jest gest użytkownika, korzystając z opcji allowWithoutGesture. Domyślna wartość tego parametru zależy od przeglądarki, dlatego zawsze należy go uwzględniać.

W tym miejscu asynchroniczny charakter interfejsu Clipboard API okazuje się bardzo przydatny: próba odczytu lub zapisu danych w schowku systemowym automatycznie wyświetla użytkownikowi prośbę o udzielenie uprawnień, jeśli nie zostały one jeszcze przyznane. Ponieważ interfejs API opiera się na obietnicach, jest to całkowicie przejrzyste, a użytkownik odmówiający przyznania dostępu do schowka powoduje odrzucenie obietnic, dzięki czemu 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ą. Jest na to sposób: odłóż dostęp do schowka za pomocą setTimeout(), a potem szybko kliknij stronę, aby ją zaznaczyć, zanim funkcje zostaną wywołane:

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

Integracja zasad dotyczących uprawnień

Aby używać interfejsu API w ramkach iframe, musisz go włączyć za pomocą zasad dotyczących uprawnień, które definiują mechanizm umożliwiający selektywne włączanie i wyłączanie różnych funkcji przeglądarki oraz interfejsów API. W zależności od potrzeb aplikacji musisz przekazać clipboard-read lub clipboard-write (lub oba te parametry).

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

Wykrywanie cech

Aby używać interfejsu Async Clipboard API we wszystkich przeglądarkach, przetestuj, czy navigator.clipboard działa, a w przeciwnym razie użyj wcześniejszych metod. Oto przykład implementacji wklejania, która obejmuje 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 dostępne były różne sposoby kopiowania i wklejania w przeglądarkach. Większość przeglądarek umożliwia kopiowanie i wklejanie za pomocą skrótów document.execCommand('copy') i document.execCommand('paste'). Jeśli tekst, który ma zostać skopiowany, jest ciągiem znaków, który nie występuje w DOM, musi zostać wstrzyknięty do DOM i wybrany:

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

Prezentacje

W poniżej zamieszczonych prezentacjach możesz zapoznać się z interfejsem Async Clipboard API. Na Glitch możesz zremiksować demo tekstu lub demo obrazu, aby eksperymentować z tymi materiałami.

Pierwszy przykład pokazuje przenoszenie tekstu do schowka i z niego.

Aby wypróbować interfejs API na przykładzie obrazów, skorzystaj z tej wersji demonstracyjnej. Pamiętaj, że obsługiwane są tylko pliki PNG i tylko w kilku przeglądarkach.

Podziękowania

Asynchroniczny interfejs API schowka został wdrożony przez Darwina Huanga i Gary'ego Kačmarčíka. Darwin przekazał też wersję demonstracyjną. Dziękujemy Kyarik i ponownie Gary’emu Kačmarčíkowi za sprawdzenie części tego artykułu.

Baner powitalny autorstwa Markusa Winklera na kanale Unsplash.