Détection du navigateur à l'aide de User-Agent
==============================================

Afficher des pages web ou des services en fonction du navigateur est généralement une mauvaise idée. Le Web se doit d'être accessible à tout le monde, sans prendre en compte le navigateur ou l'appareil utilisé. Il existe différentes façons de développer votre site web afin de l'améliorer progressivement en se basant sur des fonctionnalités standard plutôt qu'en traitant chaque navigateur de manière spécifique.

Les navigateurs et les standards ne sont cependant pas parfaits, il reste certains cas limites pour lesquels connaître le navigateur utilisé peut s'avérer utile. Utiliser la chaîne de caractères fournie par l'en-tête HTTP `User-Agent` (en-US) et disponible en JavaScript via la propriété `navigator.userAgent` (en-US) dans ce but paraît simple, mais le faire de manière fiable est en réalité très difficile. Ce document va vous guider pour le faire aussi correctement que possible.

**Note :** Il est important de rappeler qu'utiliser le contenu de l'en-tête `User-Agent` est rarement une bonne idée. Il est presque toujours possible de trouver une solution plus générique et compatible avec un plus grand nombre de navigateurs et d'appareils !

Considérations à prendre en compte avant d'identifier le navigateur
-------------------------------------------------------------------

Lorsque vous cherchez à analyser le contenu de la chaîne de caractères de l'en-tête `User-Agent` pour détecter le navigateur utilisé, la première étape consiste à éviter cette méthode autant que possible. Commencez par identifier **pourquoi** vous souhaitez le faire.

Êtes-vous en train d'essayer de corriger un bogue pour une version spécifique d'un navigateur ?

Recherchez ou demandez sur les forums spécialisés : vous n'êtes certainement pas la première ou le premier à rencontrer le problème. Des expertes, experts ou d'autres personnes avec un point de vue différent peuvent vous donner des idées pour contourner le problème. Si le bogue n'est pas fréquent, il peut être utile de vérifier s'il a déjà été signalé à l'éditeur du navigateur dans son système de suivi des bogues (Mozilla, WebKit, Opera). Les éditeurs sont attentifs aux bogues signalés, leur analyse du problème peut apporter un éclairage nouveau permettant de le contourner.

Cherchez-vous à vérifier l'existence d'une fonctionnalité particulière ?

Votre site a besoin d'une fonctionnalité qui n'est pas encore prise en charge par certains navigateurs et vous souhaitez servir à leurs utilisateurs une version plus ancienne du site, avec moins de fonctionnalités mais pour lesquelles vous avez la certitude de leur fonctionnement. Il s'agit de la pire raison pour utiliser l'en-tête `User-Agent`, car il y a de grandes chances que ces navigateurs finissent par rattraper leur retard, qu'il n'est pas pratique de tester tous les navigateurs qui existent. Dans ce cas, le mieux est d'éviter d'utiliser l'en-tête `User-Agent` et de détecter les fonctionnalités disponibles.

Voulez-vous servir un code HTML différent selon le navigateur utilisé ?

Il s'agit généralement d'une mauvaise pratique, mais qui peut être nécessaire dans certains cas. Vous devez alors analyser la situation pour vous assurer que c'est absolument nécessaire. Pouvez-vous l'éviter en ajoutant des éléments non sémantiques tels que `<div>` ou `<span>` ? La difficulté et les risque à utiliser l'en-tête `User-Agent` justifie des exceptions à la pureté du code HTML. Vous pouvez aussi repenser le design : pouvez-vous plutôt utiliser l'amélioration progressive ou utiliser une disposition flexible pour éviter d'y avoir recours ?

Éviter de détecter l'agent utilisateur
--------------------------------------

Il existe des options possibles à considérer pour éviter d'avoir à détecter l'agent utilisateur.

Détection de fonctionnalités

La détection de fonctionnalités consiste à ne pas détecter quel navigateur affiche la page mais plutôt à vérifier qu'une fonctionnalité est disponible. Dans le cas contraire vous pouvez utiliser une solution de contournement. Dans les rares cas où les comportements des fonctionnalités varient entre les navigateurs, on évitera d'analyser l'en-tête `User-Agent` et on implémentera plutôt un test permettant de détecter la façon dont le navigateur implémente l'API afin de déterminer comment l'utiliser dans son programme. En 2017, Chrome a retiré la préférence masquant la prise en charge expérimentale des références arrières dans les expressions rationnelles alors qu'aucun autre navigateur n'implémentait cette fonctionnalité. On aurait pu être tenté⋅e d'écrire ceci :


```js
if (navigator.userAgent.indexOf("Chrome") !== -1) {
  // On pense que les références arrières sont prises en charge
  // Attention à ne pas utiliser la notation littérale /(?<=[A-Z])/,
  // car cela entraînerait une erreur de syntaxe pour les navigateurs
  // qui n'implémentent pas cette fonctionnalité. En effet, les
  // navigateurs analysent le script en entier, y compris les
  // branches du code qui ne sont jamais utilisées.
  var camelCaseExpression = new RegExp("(?<=[A-Z])");
  var splitUpString = function (str) {
    return ("" + str).split(camelCaseExpression);
  };
} else {
  /\* Ce code alternatif est bien moins performant mais fonctionne \*/
  var splitUpString = function (str) {
    return str.replace(/[A-Z]/g, "z$1").split(/z(?=[A-Z])/g);
  };
}
console.log(splitUpString("totoTruc")); // ["totoT", "ruc"]
console.log(splitUpString("jQWhy")); // ["jQ", "W", "hy"]
```


Le code qui précède se base sur plusieurs hypothèses incorrectes. Tout d'abord que, parce que la chaîne de caractères `userAgent` contient `"Chrome"`, le navigateur est Chrome. Ce n'est pas le cas, les chaînes `userAgent` contiennent de nombreuses sources de confusion.

On a ensuite l'hypothèse que la fonctionnalité en question est toujours disponible si le navigateur est Chrome. Or, il peut s'agir d'une version antérieure où la fonctionnalité n'était pas encore disponible voire, plus tard, d'une version ultérieure où la fonctionnalité a fini par être retirée.

Enfin, ce code part du principe qu'aucun autre navigateur ne prendra jamais en charge cette fonctionnalité. Si un autre navigateur implémentait cette fonctionnalité, l'utilisation de script forcerait à ignorer cette possibilité.

Pour éviter ce type de problèmes, on pourra tester la présence même de la fonctionnalité :


```js
let isLookBehindSupported = false;

try {
  new RegExp("(?<=)");
  isLookBehindSupported = true;
} catch (err) {
  // Si l'agent utilisateur ne prend pas en charge cette
  // fonctionnalité, la tentative de création ci-avant
  // échouera et déclenchera une erreur et
  // isLookBehindSupported restera à false.
}

const splitUpString = isLookBehindSupported
  ? function (str) {
      return ("" + str).split(new RegExp("(?<=[A-Z])"));
    }
  : function (str) {
      return str.replace(/[A-Z]/g, "z$1").split(/z(?=[A-Z])/g);
    };
```


Comme le code précédent le montre, il y a **toujours** un moyen de tester la prise en charge d'un navigateur sans chercher à analyser la chaîne `userAgent`. Ce n'est **jamais** une bonne raison pour utiliser cette information.

Enfin, le code précédent illustre un problème critique avec le développement pour les différents navigateurs qui doit toujours être pris en compte. Il ne faut pas utiliser, de façon non-intentionnelle, les API qu'on teste dans les navigateurs incompatibles. Cela peut sembler simple, mais ce n'est pas toujours le cas. Dans l'exemple qui précède, l'utilisation d'une expression rationnelle littérale (par exemple `/reg/igm`) et qui utilise des références arrières provoquera une erreur d'analyse du code dans les navigateurs qui ne les prennent pas en charge. Aussi, il faut utiliser la forme `new RegExp("(?<=truc_arrière)");` plutôt que `/(?<=look_behind_stuff)/`, même dans la section du code qui traite des navigateurs compatibles.

Amélioration progressive

Cette technique de conception signifie séparer la page web en couches, en utilisant une approche ascendante, en commençant par une couche simple (avec peu ou pas de fonctionnalités) puis en améliorant les capacités par couches successives, chacune comportant plus de fonctionnalités.

Dégradation élégante

Il s'agit d'une approche descendante, avec laquelle on construit le site avec toutes les fonctionnalités souhaitées, pour ensuite le faire fonctionner sur des navigateurs plus anciens. Cette technique est plus difficile et moins efficace que l'amélioration progressive mais s'avère utile dans certains cas.

Détection des appareils mobiles

Un des cas les plus fréquents de mauvaise utilisation de la chaîne `userAgent` porte sur le caractère mobile ou non de l'appareil. La plupart du temps, cette méthode rapide occulte l'information réellement recherchée. L'analyse de la chaîne `userAgent` est utilisée pour déterminer si l'appareil peut être tactile et s'il a un petit écran, afin d'adapter le site web. Bien que cette méthode puisse détecter ces caractéristiques dans certains cas, tous les appareils ne se ressemblent pas : certains appareils mobiles ont des grands écrans, certains ordinateurs de bureau ont un petit écran tactile, d'autres encore sont des télévisions et les gens peuvent changer les dimensions de leur écran en tournant leur tablette sur le côté Heureusement, il existe de bien meilleures alternatives. On pourra utiliser `Navigator.maxTouchPoints` (en-US) afin de déterminer si l'appareil possède un écran tactile et ensuite seulement se rabattre sur la vérification de la chaîne `userAgent` dans un bloc *if (!("maxTouchPoints" in navigator)) { /\*Code here\*/}*. En utilisant cette information sur la présence d'un écran tactile, il n'est pas nécessaire de changer toute la disposition du site pour ces appareils : cela ne fera qu'augmenter la charge de maintenance. À la place, vous pouvez ajouter de quoi rendre la navigation tactile plus accessible avec des boutons plus facilement cliquables par exemple (en utilisant CSS pour augmenter la taille de la police). Voici un exemple de code qui augmente le remplissage de #boutonExemple jusqu'à `1em` sur les appareils mobiles.


```js
var hasTouchScreen = false;
if ("maxTouchPoints" in navigator) {
  hasTouchScreen = navigator.maxTouchPoints > 0;
} else if ("msMaxTouchPoints" in navigator) {
  hasTouchScreen = navigator.msMaxTouchPoints > 0;
} else {
  var mQ = window.matchMedia && matchMedia("(pointer:coarse)");
  if (mQ && mQ.media === "(pointer:coarse)") {
    hasTouchScreen = !!mQ.matches;
  } else if ("orientation" in window) {
    hasTouchScreen = true; // dépréciée mais utile au cas où
  } else {
    // en dernier recours, on regarde userAgent
    var UA = navigator.userAgent;
    hasTouchScreen =
      /\b(BlackBerry|webOS|iPhone|IEMobile)\b/i.test(UA) ||
      /\b(Android|Windows Phone|iPad|iPod)\b/i.test(UA);
  }
}
if (hasTouchScreen)
  document.getElementById("boutonExemple").style.padding = "1em";
```


En ce qui concerne la taille de l'écran, on utilisera `window.innerWidth` et `window.addEventListener("resize", function(){ /\*refresh screen size dependent things\*/ })`. Sur ce sujet, on ne veut pas que des informations soient masquées sur les plus petits écrans. Cela sera source de frustration et forcera à utiliser la version pour ordinateur. On essaiera plutôt d'avoir moins de colonnes d'informations sur une page plus longue pour les écrans plus étroits et une page avec plus de colonnes mais plus courte sur les écrans plus larges. On peut obtenir cet effet en CSS avec les boîtes flexibles, voire avec le flottement comme méthode alternative de recours.

Pour plus de détails, voir l'article sur le *responsive design*.

Tirer le meilleur parti de l'analyse de la chaîne `userAgent`
-------------------------------------------------------------

Après avoir vu les alternatives précédentes, il existe quelques cas où l'analyse de `userAgent` est appropriée et justifiée.

Un de ces cas est l'utilisation en méthode de dernier recours pour détecter si l'appareil dispose d'un écran tactile. Voir la section précédente pour plus d'informations.

Un autre cas porte sur la correction de bogues dans les navigateurs qui ne sont pas automatiquement mis à jour. Internet Explorer (sur Windows) et Webkit (sur iOS) sont deux bons exemples ici. Avant sa version 9, Internet Explorer avait de nombreux problèmes, mais il était simple de l'identifier en raison des fonctionnalités spécifiques disponibles. Webkit est utilisé dans tous les navigateurs sur iOS et on ne peut donc pas accéder à un navigateur mis à jour sur un appareil plus ancien. Certains bogues peuvent être détectés mais pas tous avec la même facilité. Dans de tels cas, il peut être bénéfique que d'utiliser l'analyse de `userAgent` pour économiser des performances. Par exemple, Webkit 6 a un bogue où, lorsque l'orientation de l'appareil change, le navigateur peut ne pas déclencher `MediaQueryList` (en-US) alors qu'il devrait. Pour contourner ce bogue, voyez le code qui suit.


```js
var UA = navigator.userAgent,
  isWebkit =
    /\b(iPad|iPhone|iPod)\b/.test(UA) &&
    /WebKit/.test(UA) &&
    !/Edge/.test(UA) &&
    !window.MSStream;

var mediaQueryUpdated = true,
  mqL = [];
function whenMediaChanges() {
  mediaQueryUpdated = true;
}

var listenToMediaQuery = isWebkit
  ? function (mQ, f) {
      if (/height|width/.test(mQ.media)) mqL.push([mQ, f]);
      mQ.addListener(f), mQ.addListener(whenMediaChanges);
    }
  : function () {};
var destroyMediaQuery = isWebkit
  ? function (mQ) {
      for (var i = 0, len = mqL.length | 0; i < len; i = (i + 1) | 0)
        if (mqL[i][0] === mQ) mqL.splice(i, 1);
      mQ.removeListener(whenMediaChanges);
    }
  : listenToMediaQuery;

var orientationChanged = false;
addEventListener(
  "orientationchange",
  function () {
    orientationChanged = true;
  },
  PASSIVE\_LISTENER\_OPTION,
);

addEventListener(
  "resize",
  setTimeout.bind(
    0,
    function () {
      if (orientationChanged && !mediaQueryUpdated)
        for (var i = 0, len = mqL.length | 0; i < len; i = (i + 1) | 0)
          mqL[i][1](mqL[i][0]);
      mediaQueryUpdated = orientationChanged = false;
    },
    0,
  ),
);
```

Où se trouve l'information recherchée dans le User-Agent
--------------------------------------------------------

C'est la partie difficile, puisque les différentes sections de la chaîne `User-Agent` ne sont pas standardisées.

### Nom du navigateur

Souvent ceux qui disent vouloir détecter le navigateur veulent en fait détecter le moteur de rendu. Souhaitez-vous détecter Firefox et non Seamonkey, ou Chrome et non Chromium ? Ou seulement savoir si le navigateur utilise le moteur de rendu Gecko ou Webkit ? Dans ce dernier cas, voyez plus bas dans cette page.

La plupart des navigateurs notent leur nom et version suivant le format *NomDuNavigateur/NuméroDeVersion*, à l'exception notable d'Internet Explorer. Le nom n'est cependant pas la seule information du User-Agent qui respecte ce format, il n'est donc pas possible d'y trouver directement le nom du navigateur, seulement de vérifier si le nom recherché est présent ou non. Attention certains navigateurs mentent : par exemple, Chrome mentionne à la fois Chrome et Safari dans `User-Agent`. Pour détecter Safari il faut donc vérifier que la chaîne "Safari" est présente et que "Chrome" est absent. De la même façon, Chromium se présente souvent comme Chrome et Seamonkey comme Firefox.

Faites aussi attention à ne pas utiliser une expression rationnelle trop simple sur le nom du navigateur, car `User-Agent` contient d'autres chaînes de caractères ne respectant pas le format clé/valeur. Par exemple, `User-Agent` pour Safari et Chrome contient une chaîne "like Gecko".

| Moteur | Doit contenir | Ne doit pas contenir |
| --- | --- | --- |
| Firefox | `Firefox/xyz` | `Seamonkey/xyz` |
| Seamonkey | `Seamonkey/xyz` |  |
| Chrome | `Chrome/xyz` | `Chromium/xyz` |
| Chromium | `Chromium/xyz` |  |
| Safari | `Safari/xyz` | `Chrome/xyz` ou `Chromium/xyz` |
| Opera 15+ (Blink) | `OPR/xyz` |  |
| Opera 12- (Presto) | `Opera/xyz` |  |
| Internet Explorer 10- | `; MSIE xyz;` |  |
| Internet Explorer 11 | `Trident/7.0; .*rv:xyz` |  |

[1] Safari fournit deux numéros de version : un numéro technique avec le fragment `Safari/xyz` token, et un numéro grand public avec le fragment `Version/xyz`.

Il n'y a évidemment aucune garantie qu'aucun autre navigateur ne va utiliser ces notations (comme Chrome qui mentionne "Safari" dans son User-Agent). C'est pourquoi la détection du navigateur par ce moyen n'est pas fiable et ne doit être fait qu'en vérifiant aussi le numéro de version (il est peu probable qu'un navigateur mentionne dans son User-Agent le nom d'un autre navigateur dans une version plus ancienne).

### Version du navigateur

La version du navigateur est souvent, mais pas toujours, écrite dans la valeur d'un ensemble clé/valeur *NomDuNavigateur/NuméroDeVersion* dans la chaîne de caractères `User-Agent`. Ce n'est pas le cas d'Internet Explorer (qui écrit son numéro de version juste après la chaîne "MSIE"), et d'Opera après la version 10, qui ajoute une section *Version/NuméroDeVersion*.

Encore une fois, assurez vous de regarder au bon endroit selon le navigateur visé car il n'y a aucune garantie de trouver un numéro de version valide dans le reste de la chaîne.

### Moteur de rendu

Comme indiqué plus haut, chercher le nom du moteur de rendu est la plupart du temps la meilleure solution. Cela permet de ne pas exclure des navigateurs peu connus basés sur le même moteur de rendu qu'un autre plus connu. Les navigateurs qui utilisent le même moteur de rendu affichent les pages de la même façon : on peut partir du principe que ce qui va fonctionner avec l'un fonctionnera avec l'autre.

Il y a cinq principaux moteurs de rendu : Trident, Gecko, Presto, Blink et Webkit. Puisque détecter le nom du moteur de rendu est courant, d'autres noms sont ajoutés dans beaucoup d'autres chaînes `User-Agent`. Il est donc important de faire attention aux faux positifs lorsqu'on cherche à détecter le moteur de rendu.

| Moteur | Doit contenir | Commentaire |
| --- | --- | --- |
| Gecko | `Gecko/xyz` |  |
| WebKit | `AppleWebKit/xyz` | Attention, les navigateurs WebKit ajoutent une chaîne 'like Gecko' qui peut produire des faux positifs. |
| Presto | `Opera/xyz` | **Note :** Presto n'est plus utilisé pour les versions d'Opera >= 15 (voir 'Blink') |
| Trident | `Trident/xyz` | Internet Explorer place ce fragment dans la partie *commentaires* de la chaîne `User-Agent` |
| EdgeHTML | `Edge/xyz` | La version de Edge non-basée sur Chromium indique la version du moteur après le fragment `Edge/`, mais pas la version de l'application. **Note :** EdgeHTML n'est plus utilisé pour le navigateur Edge après la version 79 (voir 'Blink'). |
| Blink | `Chrome/xyz` |  |

Version du moteur de rendu
--------------------------

La plupart des moteurs de rendu placent leur numéro de version dans la section *MoteurDeRendu/NuméroDeVersion*, à l'exception notable de Gecko. Gecko place le numéro de version dans la partie commentaire après la chaîne `rv:`. Depuis la version 14 pour mobile et 17 pour les ordinateurs, il place aussi cette valeur dans la section `Gecko/version` (les versions précédentes y plaçaient la date de compilation, puis une date fixe appelée « Gecko Trail »).

Système d'exploitation
----------------------

Le système d'exploitation est dans la plupart des cas donné dans le User-Agent mais sous un format très variable. C'est une chaîne encadrée par des points-virgules, dans la partie commentaire de la chaîne `User-Agent`. Cette chaîne est spécifique à chaque navigateur. Elle indique le nom du système d'exploitation et souvent sa version et des informations sur l'architecture (32 ou 64 bits, ou Intel/PPC pour Mac).

Comme pour le reste, ces chaînes peuvent changer dans le futur, elles doivent seulement être utilisées en conjonction avec la détection de navigateurs existants. Une veille technologique doit s'effectuer pour adapter le script de détection lorsque de nouvelles versions des navigateurs sortent.

### Mobile, tablette ou ordinateur

La raison la plus courante de détecter le User-Agent et de déterminer sur quel type d'appareil fonctionne le navigateur. Le but est de servir un code HTML différent selon le type d'appareil.

* Ne partez jamais du principe qu'un navigateur ne fonctionne que sur un seul type d'appareil. En particulier, ne pas définir de paramètre par défaut selon le navigateur.
* N'utilisez jamais la chaîne dédiée au système d'exploitation pour déterminer si le navigateur est sur un mobile, une tablette ou un ordinateur. Le même système d'exploitation peut fonctionner sur plusieurs types d'appareil (par exemple, Android fonctionne aussi bien sur des tablettes que sur des téléphones).

Le tableau suivant résume de quelle façon les principaux navigateurs indiquent qu'ils fonctionnent sur un appareil mobile :

| Navigateur | Rule | Exemple |
| --- | --- | --- |
| Mozilla (Gecko, Firefox) | `Mobile` ou `Tablet` dans le commentaire. | `Mozilla/5.0 (Android; Mobile; rv:13.0) Gecko/13.0 Firefox/13.0` |
| Basé sur WebKit (Android, Safari) | Fragment `Mobile Safari` en dehors du commentaire. | `Mozilla/5.0 (Linux; U; Android 4.0.3; de-ch; HTC Sensation Build/IML74K) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30` |
| Basé sur Blink (Chromium, Google Chrome, Opera 15+, Edge pour Android) | Fragment `Mobile Safari` en dehors du commentaire. | `Mozilla/5.0 (Linux; Android 4.4.2); Nexus 5 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.117 Mobile Safari/537.36 OPR/20.0.1396.72047` |
| Presto-based (Opera 12-) | Fragment `Opera Mobi/xyz` dans le commentaire. | `Opera/9.80 (Android 2.3.3; Linux; Opera Mobi/ADR-1111101157; U; es-ES) Presto/2.9.201 Version/11.50` |
| Internet Explorer | Fragment `IEMobile/xyz` dans le commentaire. | `Mozilla/5.0 (compatible; MSIE 9.0; Windows Phone OS 7.5; Trident/5.0; IEMobile/9.0)` |
| Edge sur Windows 10 Mobile | Fragments `Mobile/xyz` et `Edge/` en dehors du commentaire. | `Mozilla/5.0 (Windows Phone 10.0; Android 6.0.1; Xbox; Xbox One) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Mobile Safari/537.36 Edge/16.16299` |

En résumé, nous recommandons de chercher la chaîne `Mobi` dans la chaîne `User-Agent` pour détecter un appareil mobile.

> 
> **Note:** Si l'appareil est suffisamment grand pour ne pas être indiqué `Mobi`, il est préférable de servir la version du site pour ordinateur. De toute manière, supporter les interactions tactiles pour un site « pour ordinateur » est une bonne pratique. En effet, de plus en plus d'ordinateurs sont équipés d'écrans tactiles.
> 
> 
>