Spójność aktywacji użytkowników we wszystkich interfejsach API

Mustaq Ahmed
Jan Kowalski
Joe Medley

Aby uniemożliwić złośliwym skryptom nadużywanie poufnych interfejsów API, takich jak wyskakujące okienka, przeglądarki pełnoekranowe itp., przeglądarki kontrolują dostęp do tych interfejsów przez aktywację użytkownika. Aktywacja użytkownika to stan sesji przeglądania w odniesieniu do działań użytkownika: stan „aktywna” zwykle oznacza, że użytkownik wchodzi w interakcję ze stroną lub zakończył działanie od momentu jej wczytania. Gest użytkownika to popularne, ale wprowadzające w błąd określenie tego samego pomysłu. Na przykład gest przesunięcia lub przesunięcia palcem przez użytkownika nie aktywuje strony, więc z punktu widzenia skryptu nie stanowi aktywacji użytkownika.

Główne przeglądarki wykazują obecnie bardzo rozbieżne zachowanie dotyczące sposobu, w jaki aktywacja użytkownika zarządza interfejsami API z aktywacją. W Chrome wdrożenie opierało się na modelu opartym na tokenach, który okazał się zbyt złożony, aby zdefiniować spójne zachowanie we wszystkich interfejsach API ograniczonych aktywacją. Na przykład Chrome nie zezwalał na niepełny dostęp do interfejsów API objętych aktywacją za pomocą wywołań postMessage() i setTimeout(), natomiast aktywacja użytkowników nie była obsługiwana w ramach Promises, XHR czy interakcji z grami. Niektóre z tych błędów są popularne, ale występują już od dawna.

W wersji 72 Chrome udostępnia wersję 2, która zapewnia pełną aktywację użytkowników we wszystkich interfejsach API objętych aktywacją. Wyeliminuje to wspomniane powyżej niespójności (i kilka innych, np. MessageChannels), które naszym zdaniem ułatwiłyby opracowywanie treści w internecie na temat aktywacji użytkowników. Nowa implementacja stanowi także odniesienie do proponowanej nowej specyfikacji, która na dłuższą metę ma na celu połączenie wszystkich przeglądarek.

Jak działa Aktywacja użytkownika w wersji 2?

Nowy interfejs API zachowuje dwubitowy stan aktywacji użytkownika w każdym obiekcie window w hierarchii ramek: bit zapamiętujący historyczny stan aktywacji użytkownika (jeśli ramka kiedykolwiek zarejestrowano aktywację użytkownika) i krótszy bit dla bieżącego stanu (jeśli ramka zarejestrowała aktywację użytkownika w ciągu mniej więcej sekundy). Bitwa przyklejona nigdy nie resetuje się po ustawieniu klatki. Bit przejściowy jest ustawiany przy każdej interakcji użytkownika i jest resetowany po okresie wygaśnięcia (około sekundy) lub przez wywołanie interfejsu API obsługującego aktywację (np. window.open()).

Pamiętaj, że różne interfejsy API ograniczone aktywacją wymagają aktywacji użytkownika w różny sposób. Nowy interfejs API nie zmienia żadnych związanych z nimi działań. Przykład: dozwolone jest tylko 1 wyskakujące okienko na użytkownika, ponieważ window.open() wymaga aktywacji użytkownika w dotychczasowej postaci, Navigator.prototype.vibrate() nadal działa, jeśli ramka (lub dowolna z jej ramek podrzędnych) kiedykolwiek wywołała działanie użytkownika itd.

Co się zmienia?

  • Funkcja aktywacji użytkownika w wersji 2 formalnie definiuje pojęcie widoczności aktywacji użytkownika poza granicami ramek: interakcja użytkownika z konkretną ramką aktywuje teraz wszystkie zawierające klatki (i tylko te) niezależnie od ich pochodzenia. (W Chrome 72 stosujemy tymczasowe obejście, które zwiększa widoczność wszystkich klatek tego samego pochodzenia. Usuniemy to obejście, gdy udostępnimy sposób wyraźnego przekazywania informacji o aktywacji użytkownika do ramek podrzędnych).
  • Interfejs API objęty ograniczeniami dotyczącymi aktywacji zostanie wywołany z aktywowanej ramki, ale spoza kodu modułu obsługi zdarzeń, będzie działał, o ile stan aktywacji użytkownika to „aktywny” (np. nie wygasł ani nie został wykorzystany). Przed aktywacją użytkownika w wersji 2 bezwarunkowo kończyła się ona niepowodzeniem.
  • Wielokrotne nieużywane interakcje użytkownika w wybranym przedziale czasu łączą się w pojedynczą aktywację odpowiadającą ostatniej interakcji.

Przykłady spójności w interfejsach API podlegających aktywacji

Oto 2 przykłady z wyskakującymi okienkami (otwartymi za pomocą window.open()), które pokazują, jak funkcja aktywacji użytkownika w wersji 2 zapewnia spójność działania interfejsów API wymagających aktywacji.

Połączone połączenia: setTimeout()

Ten przykład pochodzi z naszej prezentacji setTimeout(). Jeśli moduł obsługi click spróbuje otworzyć wyskakujące okienko w ciągu sekundy, powinno się to udać, niezależnie od tego, jak kod „tworzy” opóźnienie. Aktywacja użytkownika w wersji 2 spełnia to oczekiwanie, więc każdy z tych modułów obsługi zdarzeń otwiera wyskakujące okienko w click (z opóźnieniem 100 ms):

function popupAfter100ms() {
  setTimeout(callWindowOpen, 100);
}

function asyncPopupAfter100ms() {
  setTimeout(popupAfter100ms, 0);
}

someButton.addEventListener('click', popupAfter100ms);
someButton.addEventListener('click', asyncPopupAfter100ms);

Bez aktywacji użytkownika w wersji 2 drugi moduł obsługi zdarzeń kończy się niepowodzeniem we wszystkich testowanych przeglądarkach. (Nawet pierwszy z nich w niektórych przypadkach kończy się niepowodzeniem).

Połączenia z postMessage() między domenami

Oto przykład z naszej demonstracji postMessage(). Załóżmy, że moduł obsługi click w ramce podrzędnej z innych domen wysyła 2 komunikaty bezpośrednio do ramki nadrzędnej. Ramka nadrzędna powinna mieć możliwość otwarcia wyskakującego okienka po otrzymaniu jednej z tych wiadomości (ale nie obu):

// Parent frame code
window.addEventListener('message', e => {
  if (e.data === 'open_popup' && e.origin === child_origin)
    window.open('about:blank');
});

// Child frame code:
someButton.addEventListener('click', () => {
  parent.postMessage('hi_there', parent_origin);
  parent.postMessage('open_popup', parent_origin);
});

Bez aktywacji użytkownika w wersji 2 ramka nadrzędna nie może otworzyć wyskakującego okienka po otrzymaniu drugiej wiadomości. Nawet pierwsza wiadomość kończy się niepowodzeniem, jeśli jest „połączona” z inną ramką w innej domenie (czyli gdy pierwszy odbiorca przekazuje ją do innej).

Działa to w przypadku aktywacji użytkownika w wersji 2 zarówno w postaci oryginalnej, jak i w łańcuchu.