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, le plein écran, etc., les navigateurs contrôlent l'accès à ces API via l'activation des utilisateurs. 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 avec la page actuellement ou a effectué une interaction depuis le chargement de la page. Le geste de l'utilisateur est un terme populaire, mais trompeur qui désigne la même idée. Par exemple, un geste de balayage ou d'effleurement d'un utilisateur n'active pas une page. Par conséquent, du point de vue du script, il ne s'agit pas d'une activation utilisateur.

Aujourd'hui, les principaux navigateurs présentent des comportements très divergents concernant la façon dont l'activation par l'utilisateur contrôle les API dont l'activation est contrôlée. Dans Chrome, l'implémentation reposait 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 dont l'activation est contrôlée. Par exemple, Chrome autorisait un accès incomplet aux API dont l'activation est contrôlée via les appels postMessage() et setTimeout(). L'activation des utilisateurs n'était pas compatible avec les promesses, XHR, les interactions avec une manette de jeu, etc. Notez que certains de ces bugs sont populaires, mais depuis longtemps.

Dans la version 72, Chrome propose User Activation v2, qui rend l'activation utilisateur complète pour toutes les API dont l'activation est contrôlée. Cela résout les incohérences mentionnées ci-dessus (et quelques autres, comme MessageChannels), qui, selon nous, faciliteraient 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 qui vise à rassembler tous les navigateurs à long terme.

Comment fonctionne User Activation v2 ?

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

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

Ce qui change

  • User Activation v2 formalise la notion de visibilité de l'activation utilisateur au-delà des limites des frames: une interaction de l'utilisateur avec un frame particulier active désormais tous les frames qui les contiennent (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 aux sous-cadres.)
  • Lorsqu'une API dont l'activation est contrôlée 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" (c'est-à-dire qu'elle n'a pas expiré ni été utilisée). Avant l'activation de l'utilisateur v2, l'opération échouait sans condition.
  • Plusieurs interactions utilisateur inutilisées au cours de l'intervalle d'expiration sont fusionnées en une seule activation correspondant à la dernière interaction.

Exemples de cohérence dans les API avec contrôle d'activation

Voici deux exemples de fenêtres pop-up (ouvertes avec window.open()) qui montrent comment User Activation v2 assure la cohérence du comportement des API dont l'activation est contrôlée.

Appels setTimeout() enchaînés

Cet exemple est tiré 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 manière dont le code "compose" le délai. User Activation v2 répond à cette attente. Chacun des gestionnaires d'événements suivants ouvre donc un pop-up sur 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 instance é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 d'origines multiples envoie deux messages directement au frame parent. Le frame parent doit pouvoir ouvrir un pop-up lorsqu'il reçoit 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 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 transfère le message à une autre).

Cela fonctionne avec User Activation v2, à la fois sous sa forme d'origine et avec la chaîne.