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 les API sensibles telles que les pop-ups, le plein écran, etc., les navigateurs contrôlent l'accès à ces API via l'activation de l'utilisateur. L'activation de l'utilisateur correspond à l'état d'une session de navigation en fonction des 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. Geste de l'utilisateur est un terme populaire, mais trompeur, désignant la même idée. Par exemple, un geste de balayage ou de glissement d'un doigt n'active pas une page. Du point de vue du script, il ne s'agit donc pas d'une activation par l'utilisateur.

Aujourd'hui, les principaux navigateurs présentent un comportement très différent quant à la manière dont l'activation de l'utilisateur contrôle les API contrôlées par l'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 dans toutes les API soumises à l'activation. Par exemple, Chrome a autorisé un accès incomplet aux API contrôlées par l'activation via des postMessage() et appels setTimeout(). L'activation des utilisateurs n'était pas compatible avec les promesses, XHR, les interactions avec la manette, etc. Notez que certains de ces bugs sont courants, mais de longue date.

Dans la version 72, Chrome propose User Activation v2, qui permet d'activer l'activation des utilisateurs pour toutes les API dont l'activation est soumise. Cela résout les incohérences mentionnées ci-dessus (et de quelques autres, comme MessageChannels), qui, selon nous, faciliterait le développement Web concernant l'activation des utilisateurs. De plus, la nouvelle mise en œuvre fournit une mise en œuvre de référence pour une nouvelle spécification proposée, qui vise à rassembler tous les navigateurs à long terme.

Comment fonctionne User Activation v2 ?

La nouvelle API conserve un état d'activation de l'utilisateur sur 2 bits pour chaque objet window de la hiérarchie des frames: un bit persistant pour l'état d'activation de l'utilisateur historique (si une frame a déjà vu une activation de l'utilisateur) et un bit temporaire pour l'état actuel (si une trame a constaté une activation de l'utilisateur depuis une seconde environ). Une fois défini, le sticky bit ne se réinitialise jamais pendant la durée de vie du frame. Le bit temporaire est défini à chaque interaction utilisateur et est réinitialisé après un intervalle d'expiration (environ une seconde) ou via un appel à une API consommant l'activation (par exemple, window.open()).

Notez que les différentes API dont l'activation est contrôlée dépendent de l'activation de l'utilisateur de manière différente. La nouvelle API ne modifie aucun de ces comportements spécifiques aux API. Par exemple, un seul pop-up est autorisé par activation de l'utilisateur, car window.open() utilise l'activation de l'utilisateur comme avant, Navigator.prototype.vibrate() continue d'être efficace si une action de l'utilisateur a déjà été effectuée sur un frame (ou l'un de ses sous-frames), etc.

Ce qui change

  • User Activation v2 formalise la notion de visibilité d'activation de l'utilisateur au-delà des limites des frames: une interaction utilisateur avec un frame particulier active désormais tous les frames associés (et uniquement ces frames) quelle que soit leur origine. (Dans Chrome 72, nous avons mis en place une solution temporaire pour étendre la visibilité à tous les frames de même origine. Nous supprimerons cette solution de contournement une fois que nous aurons un moyen de transmettre explicitement l'activation de l'utilisateur à des sous-frames.)
  • Lorsqu'une API soumise à une activation est appelée à partir d'une trame activée, mais depuis l'extérieur du code d'un gestionnaire d'événements, elle fonctionne tant que l'état d'activation de l'utilisateur est "active" (c'est-à-dire qu'elle n'a pas expiré ni n'a été utilisée). Avant l'activation d'utilisateur v2, l'opération échouait sans condition.
  • Plusieurs interactions utilisateur inutilisées au cours de l'intervalle d'expiration correspondent à une seule activation correspondant à la dernière interaction.

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

Voici deux exemples avec des fenêtres pop-up (ouvertes avec window.open()) qui montrent comment User Activation v2 rend le comportement des API contrôlées par activation cohérent.

setTimeout() appels en chaîne

Cet exemple est issu de notre démonstration setTimeout(). Si un gestionnaire click tente d'ouvrir un pop-up en l'espace d'une seconde, il devrait réussir, quelle que soit la façon dont le code "compose" le délai. User Activation v2 répond à ces attentes. Chacun des gestionnaires d'événements suivants ouvre donc un 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 User Activation v2, 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() interdomaines

Voici un exemple tiré de notre démonstration postMessage(). Supposons qu'un gestionnaire click dans un sous-frame multi-origine envoie deux messages directement à la trame parente. Le frame parent doit pouvoir ouvrir un pop-up à la réception de l'un de ces messages (mais pas des 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 User Activation v2, 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é" à une autre trame multi-origine (en d'autres termes, si le premier destinataire le transfère à un autre).

Cela fonctionne avec User Activation v2, à la fois dans sa forme d'origine et avec le chaînage.