lucidiot's cybrecluster

Une console JavaScript pour Internet Explorer 6, partie 2

Lucidiot Informatique 2022-09-19
La première version fonctionnelle de mes outils de développeur pauvre.


Dans l'épisode précédent, on a commencé à construire quelques fonctionnalités pour pouvoir fournir un petit sous-ensemble de l'API console moderne. Ce n'est que maintenant, en rédigeant cette introduction, que j'ai pensé à vérifier s'il y avait une spécification ; je reviendrai peut-être plus tard sur mes fonctions pour en rajouter un peu plus qui correspondent à celle-ci.

On a donc pour l'instant on a une coquille vide et des API base-niveau. On va dans cette partie relier les deux et avoir une console fonctionnelle.

Gestion des boutons

On peut implémenter console.clear, une autre méthode moderne existante, et relier directement le bouton d'effacement à cette méthode :

console.clear = function () {
  output.innerHTML = "";
}
var clearButton = document.getElementById('clearbutton');
clearButton.onclick = console.clear;

Pour ce qui est du bouton d'exécution de code, on va tout de suite le relier à une méthode run qu'on devra implémenter ensuite.

var runButton = document.getElementById('runbutton');
runButton.onclick = run;

Exécution de code

Pour exécuter du code en JS, il existe deux manières de procéder : eval et Function. eval est assez connu et permet d'exécuter du code directement dans le contexte en cours, et avec "use strict" activé va s'exécuter plutôt dans le contexte global. Function va définir du code dans une fonction et on s'attend donc à voir un return pour pouvoir récupérer quelque chose. Dans notre contexte, Function est inutilement complexe à implémenter, et on peut ignorer la majorité des avertissements de sécurité concernant eval puisque l'objectif entier de cette console est de pouvoir exécuter du code arbitraire. Ce n'est pas une vulnérabilité, c'est une feature ; sans doute le principe derrière la plupart des appareils de l'Internet of Things.

Notre fonction doit donc faire trois choses : afficher le code exécuté dans l'historique, exécuter le code, et afficher un éventuel résultat ou le message d'erreur si une erreur se produit.

var input = document.getElementById('input');
var inputLogger = makeLogger('lightblue');
function run () {
  var code = input.value;
  if (!code.trim()) return;
  inputLogger(code);
  try {
    console.log(eval(code));
  } catch (err) {
    console.error(err);
  }
}

On crée un nouveau logger de couleur bleu clair pour distinguer facilement le code exécuté des autres messages. Il ne sera pas disponible dans les console.* comme les autres. Quand la méthode run s'exécute, on commence par récupérer le code, et s'il n'y a pas de code... on ne fait rien. Sinon, on affiche le code avec notre nouveau logger. Ensuite, on exécute bêtement le code et on affiche le résultat, et en cas d'erreur, on affiche l'erreur avec console.error. Avec tout ce qu'on a mis en place précédemment, l'exécution du code devient assez triviale.

Raccourci clavier

Pour permettre d'aller un peu plus vite quand on écrit beaucoup de code, on peut rajouter à notre zone de texte une gestion des appuis de touches de clavier : si on appuie sur Shift et Entrée en même temps, alors on exécute le code. La touche Entrée doit continuer à pouvoir juste passer une ligne, et Ctrl + Entrée est parfois utilisée dans d'autres interfaces pour des significations différentes, donc on va utiliser Shift.

input.onkeypress = function (event) {
  if (!event) event = window.event;
  // Run code if Shift+Enter is pressed
  if (event && event.keyCode === 13 && event.shiftKey) {
    event.cancelBubble = true;
    event.returnValue = false;
    if (event.stopPropagation) event.stopPropagation();
    if (event.preventDefault) event.preventDefault();
    run();
  }
}

Avant Internet Explorer 9, les gestionnaires d'événements ne recevaient pas l'événement en cours comme paramètre : il fallait utiliser window.event tout court. Si on a une variable locale event, alors il faut utiliser explicitement window.event pour y avoir accès, mais sinon event tout court peut accéder à window.event, comme une variable globale ; ça peut donner lieu à du code très confus dans des vieux scripts, et utiliser cette variable globale cause désormais des avertissements dans les IDE modernes pour une "dépréciation" (bien que rien sur le Web ne soit vraiment déprécié, vu que la compatibilité bat tout le reste). On a donc cette construction étrange en début de fonction, où si on n'a pas reçu d'événement en argument, on utilise la variable globale.

Le keyCode est la seule propriété qu'on soit vraiment sûr de toujours avoir avec la même valeur dans ces navigateurs anciens, et le code 13 correspond au caractère ASCII 13, autrement dit le saut de ligne. event.shiftKey est une valeur booléenne indiquant si la touche Shift était utilisée simultanément.

Une fois qu'on a détecté l'appui des touches qu'on veut, on doit configurer l'événement de sorte à indiquer que c'est bon, on gère cet événement, et les autres gestionnaires d'événements ne doivent pas s'en occuper, y compris le gestionnaire par défaut du navigateur ; appuyer sur Shift et Entrée ne doit pas insérer de saut de ligne. On utiliserait pour cela event.preventDefault dans un navigateur moderne, ou event.stopPropagation pour éviter la propagation aux éléments parents, mais ça ne fonctionne pas dans Internet Explorer avant la version 9 non plus. Par conséquent, on utilise les attributs désormais dépréciés cancelBubble, qui stoppe le bubbling (la propagation), et returnValue, qui déclenche l'événement par défaut (l'insertion de saut de ligne) s'il est défini à true. J'ai appris ces hacks en lisant le code source de shortcuts, une librairie fournissant la gestion de raccourcis clavier et indiquant un support de IE 8 (mais cette technique fonctionne aussi jusqu'à IE 4).

Une fois qu'on a enfin réussi à prendre le contrôle de cet événement, on peut enfin exécuter le code en appellant juste run().

Exécuter le texte sélectionné

Quand j'ai commencé à expérimenter vraiment en utilisant la console, je me suis vite rendu compte que je faisais beaucoup de va-et-vient entre un éditeur de texte et la console. Je me suis rendu compte qu'en ajoutant la possibilité de n'exécuter qu'une partie du code que j'ai tapé dans la console, je pouvais éviter d'avoir besoin d'un éditeur à part la plupart du temps, alors j'ai ajouté la capacité d'exécuter du texte sélectionné, en sélectionnant d'abord le texte puis en appuyant sur Shift + Entrée.

Récupérer le texte sélectionné sous Internet Explorer 9 et ultérieur, ou avec les autres navigateurs, est en général assez simple :

var selection = window.getSelection().toString();

Pour prendre en charge Internet Explorer 8, on peut aussi utiliser cette façon :

var selection = document.selection.createRange().text;

Mais pour prendre en charge Internet Explorer 6 et 7 comme on le souhaite, on doit faire bien plus compliqué. J'ai du coup utilisé une fonction assez compliquée que j'ai trouvé sur StackOverflow et je l'ai modifiée pour qu'elle corresponde à mes besoins :

function getSelectedText (element) {
  // On a une API plus facile ! Utilisons là !
  if (typeof element.selectionStart === 'number' && typeof element.selectionEnd === 'number') {
    return element.value.substring(element.selectionStart, element.selectionEnd);
  }

  var range = document.selection.createRange();
  // Vérifions que la sélection actuelle est sur l'élément en cours
  if (!range || range.parentElement() !== element) return '';

  // Transformer les CRLF en LF pour éviter des problèmes d'index
  var normalizedValue = removeCarriageReturns(element.value);

  // Créer une sélection dans le contexte de l'élément et pas de la fenêtre,
  // ce qui permettra de comparer sa position avec le texte de l'élément
  var inputRange = element.createTextRange();
  // Copier les positions de la sélection initiale
  inputRange.moveToBookmark(range.getBookmark());

  // Créer une sélection qui se trouvera à la fin du texte
  var endRange = element.createTextRange();
  endRange.collapse(false);

  // Notre sélection commence à la fin du texte, donc elle est vide
  if (inputRange.compareEndPoints('StartToEnd', endRange) > -1) return '';

  // On récupère l'index de début de la sélection en déplaçant le début de la sélection,
  // ce qui nous fait compter les caractères jusqu'au début du texte
  var start = -inputRange.moveStart('character', -element.value.length);
  // Tenir compte d'éventuelles lignes vides
  start += normalizedValue.slice(0, start).split('\n').length - 1;

  var end = element.value.length;
  if (inputRange.compareEndPoints('EndToEnd', endRange) < 0) {
    end = -inputRange.moveEnd('character', -element.value.length);
    end += normalizedValue.slice(0, end).split('\n').length - 1;
  }

  return element.value.substring(start, end)
}

function run () {
  var code = getSelectedText(input) || input.value;
  // ...
}

Les développeurs se plaignent de devoir prendre en charge Internet Explorer, mais quand on a StackOverflow sous la main, ça ne demande finalement pas autant de travail que ça en a l'air !

Gestion de IE 7

J'ai remarqué après diverses expérimentations que dans les consoles intégrées à des instances d'Internet Explorer 7, l'affichage était un petit peu bancal ; la zone de texte débordait en bas de la fenêtre, ce qui faisait apparaître une barre de défilement, et la zone de sortie était légèrement décalée sur la gauche, ce qui rognait légèrement le premier caractère de chaque ligne. Je n'ai pas trouvé le moindre moyen de gérer ça proprement sans causer des problèmes dans les autres navigateurs, alors j'ai ajouté du code qui détecte de façon très bête l'utilisation d'IE 7, et qui modifie légèrement des propriétés de style :

window.onload = function () {
  // ...
  if (/MSIE 7.0/.test(navigator.userAgent)) {
    input.style.height = document.documentElement.clientHeight - 7 + 'px';
    output.style.marginLeft = '7px';
  }
}

C'est loin d'être idéal, et ça donnera un affichage étrange quand on redimensionne la fenêtre, mais dans un contexte tel qu'un assistant Web, qui n'est pas redimensionnable et est le premier objectif de cette console, ça fonctionne.

Gestion de console existante

À partir d'Internet Explorer 8, on commence à voir apparaître des outils pour développeurs. À cette version, ils sont encore spartiates, mais ils existent. On peut donc essayer de commencer à prendre en charge une API window.console qui existerait déjà avant que notre script ne soit chargé. Pour cela, on va devoir stocker l'objet window.console existant dans une variable avant de le réécrire :

var originalConsole = window.console;
window.console = {};

On va devoir ensuite gérer l'appel de cette console d'origine dans toutes nos méthodes de logging, et dans la méthode d'effacement. On doit aussi gérer bien sûr le cas où la console d'origine n'existe pas. On ne va pas ajouter de gestion spécifique dans console.assert() et console.trace() car nos méthodes de remplacement font appel aux méthodes de logging.

Pour gérer l'appel de la méthode d'origine dans chaque logger renvoyé par notre méta-fonction makeLogger, on va devoir ajouter un nouvel argument obligatoire, celui du nom de la méthode à appeler. makeLogger ne dispose sinon pas du contexte nécessaire pour savoir quoi vraiment faire.

function makeLogger (name, color) {
  return function () {
    if (originalConsole && originalConsole[name] && originalConsole[name].apply) originalConsole[name].apply(originalConsole, arguments);
    // ...
  }
}

On ajoute une seule ligne au début de la fonction. Si la console d'origine existe et que la méthode qu'on cherche existe, alors on l'appelle avec nos arguments d'origine. La fonction apply d'une Function prend comme arguments un objet qui deviendra la valeur de this, et un tableau contenant les arguments à utiliser. J'ai découvert en implémentant ça que console.log.apply n'existe pas dans IE 8 : la console semble être implémentée de façon très étrange, où console est un object (donc pas un contrôle ActiveX particulier), et console.info est aussi un object, et pas une function comme on s'y attendrait, et on ne peut pas en connaître les propriétés. Le problème s'applique à toutes les méthodes fournies par l'objet console, et pas juste console.info. On teste donc également que .apply est bien disponible avant d'appeler.

Il ne reste plus qu'à ajouter la même détection dans console.clear. Cette méthode n'est pas censée accepter le moindre argument, mais dans le doute, si un navigateur se décide à ajouter des arguments, je vais quand même essayer de les faire passer. Si on est dans le cas où console.clear.apply n'est pas défini, je vais appeler quand même sans aucun argument. Ça donnera au moins une chance supplémentaire de mieux prendre en charge Internet Explorer 8.

console.clear = function () {
  if (originalConsole && originalConsole.clear) {
    if (originalConsole.clear.apply) originalConsole.clear.apply(originalConsole, arguments);
    else originalConsole.clear();
  }
  output.innerHTML = "";
}

Gestion des assistants Web

Dans mon article introduisant les assistants Web de Windows XP, j'ai notamment expliqué que l'assistant allait afficher une erreur si on ne prend pas en charge l'événement window.onback, censé permettre de gérer l'appui sur le bouton Précédent dans l'assistant. De plus, les trois boutons Précédent, Suivant et Terminer sont tous désactivés tant qu'on n'utilise pas la méthode SetWizardButtons pour activer les boutons manuellement.

Puisque la console va être conçue pour s'exécuter dans moult contextes, et pas seulement les assistants Web, on va devoir ajouter du code effectuant la détection automatique d'un contexte d'assistant Web, et réagissant en conséquence.

Dans un assistant Web, on dispose d'un objet ActiveX WebWizardHost dans la propriété window.external. JScript ne dispose pas de moyen simple pour déterminer le type d'un objet ActiveX, à moins que cet objet n'implémente des méthodes spécifiques pour gérer correctement un appel à typeof. VBScript se débrouille mieux sur ce point. On ne pourrait pas utiliser instanceof puisqu'on n'a pas le type WebWizardHost lui-même disponible nulle part. On ne pourrait pas utiliser d'autres méthodes comme accéder à window.external.constructor.name, puisque constructor n'est pas implémenté par l'objet ActiveX.

Tout ce qu'on pourra se contenter de faire, c'est détecter que window.external est un objet quelconque, puis détecter que les méthodes de l'interface du WebWizardHost sont implémentées par cet objet :

if (typeof window.external === 'object') {
  if (typeof window.external.SetHeaderText === 'unknown')
    window.external.SetHeaderText('JavaScript Console', "Explore this wizard's JavaScript environment.");

  if (typeof window.external.SetWizardButtons === 'unknown')
    window.external.SetWizardButtons(1, 0, 0);

  if (typeof window.external.FinalBack === 'unknown') {
    window.onback = function () {
      window.external.FinalBack();
    }
  }
}

En plus d'activer le bouton Précédent et de gérer son appui pour ordonner de quitter la page avec FinalBack, on utilise aussi SetHeaderText pour afficher un titre et une description plus jolis dans l'assistant. On doit effectuer ce traitement en dehors de l'événement window.onload, contrairement à tout le reste du code, car l'événement window.onback doit être pris en charge très tôt pour que l'assistant ne se plaigne pas que l'événement soit manquant.

On notera que les fonctions qu'on essaie de détecter n'ont pas pour type function mais unknown. Jusqu'à ECMAScript 2019, il était possible pour les navigateurs de retourner n'importe quelle valeur dans un typeof, mais Internet Explorer était le seul à utiliser ce détail d'implémentation pour les attributs d'objets ActiveX.

Intégration dans le Registre

Pour que notre console fonctionne dans les assistants Web, on va devoir aussi l'ajouter comme un fournisseur de services dans le Registre. J'ai déjà décrit dans mon article sur les assistants Web la structure exacte à respecter, donc je vais simplement inclure ici un fichier .reg qui peut être importé directement dans le Registre pour installer la console, en supposant que vous l'avez placée dans C:\jsconsole. La structure est en triple car elle est ajoutée pour les trois assistants Web de Windows XP : l'ajout de favori réseau, l'impression de photos en ligne, et la publication de dossier sur le Web.

Windows Registry Editor Version 5.00

[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\PublishingWizard]

[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\PublishingWizard\AddNetPlace]

[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\PublishingWizard\AddNetPlace\Providers]

[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\PublishingWizard\AddNetPlace\Providers\jsconsole]
"HREF"="file:///C:\\jsconsole\\index.html"
"IconPath"="shell32.dll,24"
"DisplayName"="JavaScript Console"
"Description"="Explore this wizard's JavaScript environment."

[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\PublishingWizard\PublishingWizard]

[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\PublishingWizard\PublishingWizard\Providers]

[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\PublishingWizard\PublishingWizard\Providers\jsconsole]
"HREF"="file:///C:\\jsconsole\\index.html"
"IconPath"="shell32.dll,24"
"DisplayName"="JavaScript Console"
"Description"="Explore this wizard's JavaScript environment."

[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\PublishingWizard\InternetPhotoPrinting]

[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\PublishingWizard\InternetPhotoPrinting\Providers]

[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\PublishingWizard\InternetPhotoPrinting\Providers\jsconsole]
"HREF"="file:///C:\\jsconsole\\index.html"
"IconPath"="shell32.dll,24"
"DisplayName"="JavaScript Console"
"Description"="Explore this wizard's JavaScript environment."

Programme d'installation

Dans le dépôt Git du projet, j'ai inclus dans le dossier setup/ un projet de déploiement Visual Studio 2005 qui permet de créer un programme d'installation qui installe les fichiers HTML, CSS et JS de la console sur l'ordinateur et ajoute lui-même les clés requises dans le Registre.

Je n'ai pour l'instant pas fourni ce programme sous forme de release comme je l'ai fait pour ToeCracker ou pour tsunpack-csharp, car ce projet de console est encore en développement ; il y a encore moult autres contextes ActiveX inattendus à explorer, et pas mal de travail à faire dans ce programme d'installation.

Conclusion

Voilà qui termine le développement d'un interpréteur JavaScript pour Internet Explorer 6, 7 et 8. Dans la suite de mes bêtises sous Windows XP, on verra le développement d'un interpréteur VBScript, que j'ai réalisé en dupliquant complètement cette console, puis la réalisation qu'il est en fait tout à fait possible de fusionner les deux consoles. Dire que tout ça n'était censé être qu'un seul article, et que ce n'est que l'introduction des bricolages avec les scripts dans Internet Explorer...


Commentaires

Il n'y a pour l'instant aucun commentaire. Soyez le premier !