Rendre l'activation des utilisateurs cohérente entre les API

Mustaq Ahmed
Joe Medley
Joe Medley

Pour empêcher les scripts malveillants d'utiliser de manière abusive des API sensibles telles que les pop-ups, l'écran plein écran, etc., les navigateurs contrôlent l'accès à ces API via l'activation par l'utilisateur. L'activation de l'utilisateur correspond à l'état d'une session de navigation par rapport aux actions de l'utilisateur: un état "actif" implique généralement que l'utilisateur interagit actuellement avec la page ou qu'il a effectué une interaction depuis le chargement de la page. Le terme geste utilisateur est populaire, mais trompeur. Par exemple, un geste de balayage ou de balayage par un utilisateur n'active pas une page et n'est donc pas, du point de vue du script, une activation de l'utilisateur.

Les principaux navigateurs d'aujourd'hui présentent un comportement très divergent concernant la manière dont l'activation de l'utilisateur contrôle les API soumises à activation. Dans Chrome, l'implémentation était basée sur un modèle basé sur des jetons qui s'est avéré trop complexe pour définir un comportement cohérent pour toutes les API soumises à une activation. Par exemple, Chrome autorise un accès incomplet aux API soumises à activation via les appels postMessage() et setTimeout(). L'activation de l'utilisateur n'était pas prise en charge avec Promises, XHR, Interaction avec le gamepad, etc. Notez que certains de ces bugs sont populaires, mais anciens.

Dans la version 72, Chrome fournit la version 2 de l'activation utilisateur, qui rend l'activation utilisateur disponible pour toutes les API soumises à activation. Cela résout les incohérences mentionnées ci-dessus (et quelques autres, comme MessageChannels), ce qui, selon nous, facilitera le développement Web autour de l'activation des utilisateurs. De plus, la nouvelle implémentation fournit une implémentation de référence pour une nouvelle spécification proposée qui vise à rassembler tous les navigateurs à long terme.

Comment fonctionne la version 2 de l'activation des utilisateurs ?

La nouvelle API maintient un état d'activation utilisateur à deux bits pour chaque objet window dans la hiérarchie des cadres: un bit persistant pour l'état d'activation utilisateur historique (si un cadre a déjà enregistré une activation utilisateur) et un bit temporaire pour l'état actuel (si un cadre a enregistré une activation utilisateur en environ une seconde). Le bit sticky n'est jamais réinitialisé pendant la durée de vie du frame une fois qu'il est défini. Le bit temporaire est défini à chaque interaction de l'utilisateur et est réinitialisé soit après un intervalle d'expiration (environ une seconde), soit via un appel à une API consommant de l'activation (par exemple, window.open()).

Notez que les différentes API soumises à activation s'appuient sur l'activation de l'utilisateur de différentes manières. La nouvelle API ne modifie aucun de ces comportements spécifiques à l'API. Par exemple, une seule fenêtre pop-up est autorisée par activation de l'utilisateur, car window.open() consomme l'activation de l'utilisateur comme auparavant, Navigator.prototype.vibrate() reste efficace si un frame (ou l'un de ses sous-frames) a déjà vu une action de l'utilisateur, etc.

Ce qui change

  • La version 2 de l'activation utilisateur formalise la notion de visibilité de l'activation utilisateur au-delà des limites de frame: une interaction utilisateur avec un frame particulier active désormais tous les frames contenants (et uniquement ces frames), quelle que soit leur origine. (Dans Chrome 72, nous avons mis en place un correctif temporaire pour étendre la visibilité à tous les cadres de même origine. Nous supprimerons cette solution de contournement une fois que nous aurons trouvé un moyen de transmettre explicitement l'activation de l'utilisateur aux sous-cadres.)
  • Lorsqu'une API activée par activation est appelée à partir d'un frame activé, mais en dehors d'un code de gestionnaire d'événements, elle fonctionne tant que l'état d'activation de l'utilisateur est "actif" (par exemple, qu'il n'a pas expiré ni été consommé). Avant la version 2 de l'activation utilisateur, cette opération échouait inconditionnellement.
  • Plusieurs interactions utilisateur inutilisées au cours de l'intervalle de temps d'expiration fusionnent en une seule activation correspondant à la dernière interaction.

Exemples de cohérence dans les API à activation contrôlée

Voici deux exemples avec des fenêtres pop-up (ouvertes à l'aide de window.open()) qui montrent comment la version 2 de l'activation utilisateur rend le comportement des API soumises à activation cohérent.

Appels setTimeout() en série

Cet exemple provient de notre démonstration setTimeout(). Si un gestionnaire click tente d'ouvrir un pop-up en une seconde, il doit réussir, quelle que soit la façon dont le code "compose" le délai. La version 2 de l'activation utilisateur répond à cette attente. Par conséquent, chacun des gestionnaires d'événements suivants ouvre une fenêtre pop-up sur un click (avec un délai de 100 ms):

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

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

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

Sans la version 2 de l'activation de l'utilisateur, le deuxième gestionnaire d'événements échoue dans tous les navigateurs que nous avons testés. (Même la première échoue dans certains cas.)

Appels postMessage() multidomaines

Voici un exemple tiré de notre démonstration postMessage(). Supposons qu'un gestionnaire click dans un sous-cadre inter-origine envoie deux messages directement au cadre parent. Le frame parent doit pouvoir ouvrir une fenêtre pop-up à la réception de l'un de ces messages (mais pas les deux):

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

Sans la version 2 de l'activation de l'utilisateur, le frame parent ne peut pas ouvrir de pop-up à la réception du deuxième message. Même le premier message échoue s'il est "enchaîné" à un autre frame inter-origine (en d'autres termes, si le premier destinataire transfère le message à un autre).

Cela fonctionne avec la version 2 de l'activation utilisateur, à la fois dans la forme d'origine et avec le chaînage.