lucidiot's cybrecluster

Une console pour les gouverner tous

Lucidiot Informatique 2022-10-09
Encore pire que VBScript ou que JavaScript : VBScript et JavaScript, en même temps.


En écrivant mes précédents articles qui détaillent le développement d'outils de développement de basse qualité pour Internet Explorer 6 à 8, d'abord en JavaScript puis en VBScript, j'ai longuement fouillé dans une documentation aujourd'hui disparue des sites de Microsoft pour en savoir plus sur des fonctionnalités particulières du moteur JScript. Sur une page que je ne retrouve plus puisque je ne l'ai pas sauvegardée, j'ai découvert quelque chose qui m'a fait retirer mes lunettes, puis plaquer la paume de ma main contre ma tête dans un splendide facepalm : il est tout à fait possible d'appeler des fonctions définies en VBScript depuis du code JScript et vice-versa.

Lire cette documentation, avec un exemple tout simple d'une fonction déclarée dans un langage puis appelée dans un autre, m'a fait réaliser que j'ai mis énormément d'effort dans le développement de deux consoles distinctes alors qu'une seule console aurait parfaitement été possible. Je pourrais faire la majorité de la console en JavaScript, puisque ça s'est finalement avéré plus simple qu'avec VBScript, et si l'utilisateur choisit d'appeler du VBScript, alors on appelle une fonction en VBScript qui exécutera le code VBScript.

Un autre avantage de cette approche est qu'on pourra laisser à l'utilisateur le choix entre Eval et Execute, les deux instructions distinctes disponibles en VBScript et qui ont chacune des avantages et des inconvénients. On pourra également exposer les deux fonctions à l'utilisateur pour permettre d'exécuter du VBScript à l'intérieur de code JavaScript, pour écrire des scripts un peu plus complexes dans la console et garder les avantages des deux langages.

Structure de la console unique

Pour cette console modifiée, les deux principales différences sont qu'on a un nouvel élément <select> à côté des boutons en bas à droite, ce qui nous permettra de sélectionner un mode dans lequel exécuter le code, et qu'on a maintenant deux scripts, l'un en VBScript et l'autre en JavaScript. Côté CSS, il n'y a aucune différence particulière.

<!DOCTYPE html>
<html>
  <head>
    <title>Windows Scripting Console</title>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=8" />
    <link rel="stylesheet" type="text/css" href="console.css" />
    <link rel="icon" type="image/x-icon" href="console.ico" />
    <link rel="shortcut icon" type="image/x-icon" href="console.ico" />
  </head>
  <body>
    <textarea id="input" placeholder="Type some JavaScript to run and press Shift+Enter"></textarea>
    <div id="output"></div>
    <div class="buttons">
      <select id="languageselect">
        <option value="js">JavaScript</option>
        <option value="vbs-eval">VBScript Eval</option>
        <option value="vbs-execute">VBScript Execute</option>
      </select>
      <input type="button" id="runbutton" value="run" />
      <input type="button" id="clearbutton" value="clear" />
    </div>
    <script type="text/vbscript" language="vbscript" src="console.vbs"></script>
    <script type="text/javascript" language="javascript" src="console.js"></script>
  </body>
</html>

Notez qu'on a placé le script VBScript avant le script JavaScript. Cela nous permet d'éviter quelques problèmes potentiels liés à l'ordre d'évaluation des scripts, car on va avoir besoin d'accéder aux fonctions définies en VBS dans le script JS.

Nouvelle abstraction

J'ai choisi de me baser sur la console JavaScript, et de ne faire qu'ajouter quelques fonctionnalités en VBScript, notamment parce que j'ai pu implémenter plus de choses de l'API Console qu'en VBScript et que le typage de JavaScript est plus flexible. On va donc reprendre notre fonction run qu'on utilisait pour exécuter du code JavaScript, et on doit la modifier pour qu'elle puisse aussi exécuter du VBScript.

Pour ce faire, on va rajouter un nouveau niveau d'abstraction : la fonction run n'appellera plus directement eval() comme elle le faisait auparavant, mais appellera une fonction parmi plusieurs pour exécuter du code dans le langage souhaité. On peut donc commencer par déclarer une fonction séparée pour exécuter du JavaScript :

function JSEval (code) {
  try {
    return eval(code);
  } catch (err) {
    console.error(err);
  }
}

La fonction ne respecte pas les conventions de nommage classique de JavaScript, car j'en profite pour rendre possible le fait de l'appeller en VBScript : on pourra faire quelque chose comme MsgBox window.JSEval("parseInt('0755')"), une construction bizarre qui affichera une boîte de dialogue avec 493 sous Internet Explorer et 755 dans les navigateurs qui respectent la norme ECMAScript 5.

Notez que j'ajoute window. devant JSEval, car VBScript n'est pas capable de reconnaître que JSEval existe dans son contexte autrement. Je n'ai pas réussi à trouver d'alternative à cet appel étrange.

On peut ensuite définir une procédure et une fonction en VBScript, respectivement pour Execute et Eval. Execute ne peut pas retourner de valeur puisqu'elle exécute des instructions, mais Eval en renvoie une puisqu'elle évalue une expression.

Option Explicit
On Error Resume Next

Sub VBSExecute(code)
    Err.Clear
    Execute "On Error Resume Next" & vbCrLf & code
    If Err.Number <> 0 Then
        console.error "VBScript Error " & Err.Number & " - " & Err.Source & ": " & Err.Description
    End If
End Sub

Function VBSEval(code)
    Err.Clear
    VBSEval = Eval(code)
    If Err.Number <> 0 Then
        console.error "VBScript Error " & Err.Number & " - " & Err.Source & ": " & Err.Description
    End If
End Function

Enfin, on peut modifier la fonction JavaScript run pour la rendre capable d'exécuter l'une des trois fonctions :

window.onload = function () {
  // ...
  function run () {
    var code = getSelectedText(input) || input.value;
    if (!code.trim()) return;

    inputLogger(code);
    try {
      var result;
      switch (languageSelect.value) {
        case 'js':
          result = JSEval(code);
          break;
        case 'vbs-eval':
          result = VBSEval(code);
          break;
        case 'vbs-execute':
          result = VBSExecute(code);
          break;
        default:
          console.error('Unsupported language', languageSelect.value);
          return;
      }
      console.log(result);
    } catch (err) {
      console.error(err);
    }
  }
  // ...
}

Et voilà. Avec beaucoup moins d'efforts que dans les précédents articles, on a ajouté la prise en charge de VBScript. Il serait possible plus tard d'ajouter même une prise en charge pour d'autres langages en fonction de ceux installés par l'utilisateur, puisqu'il est possible de disposer de Python, Perl, Haskell, PHP, etc. avec les bons logiciels installés, et aussi d'intégrer ses propres langages dans les API Active Scripting.

Prise en charge des assistants Web

Dans notre script VBScript, on va conserver quand même une autre fonction qu'on avait déjà implémenté précédemment : la détection des assistants Web. On l'avait implémenté en JavaScript et en VBScript, et l'implémentation VBScript était plus explicite puisqu'elle pouvait vérifier le nom de l'interface qui fournissait les fonctions de l'assistant Web, au lieu de juste détecter les fonctions elles-mêmes sans vraiment même être sûr que ce soient des fonctions comme le faisait l'implémentation JavaScript.

Function InWebWizard()
    InWebWizard = TypeName(window.external) = "INewWDEvents"
End Function

On notera les deux = qui ont des significations différentes, une chose possible seulement parce que la syntaxe de VBScript est suffisamment restreinte pour éviter des significations ambigües ici. Le premier = est une affectation pour la valeur de retour de la fonction, et le second est un opérateur d'égalité. On peut appeler cette fonction en JavaScript tout aussi facilement qu'on appelait l'ancienne fonction JavaScript :

if (InWebWizard()) { /* ... */ }

Prise en charge des navigateurs modernes

J'ouvre occasionnellement la console dans Firefox ou SeaMonkey pour pouvoir débuguer mon code, parce que n'importe quel navigateur autre que Internet Explorer est plus confortable que celui-ci, peu importe la version d'IE. Le problème, c'est que VBScript n'est pris en charge nulle part ailleurs que dans IE. Puisqu'on utilise la fonction VB InWebWizard, et que le code JavaScript fait plusieurs fois référence aux fonctions VBScript même si elles n'existent pas, on va avoir besoin de faire quelques modifications pour détecter la présence de VBScript.

On va devoir revenir aux bonnes vieilles méthodes qu'on avait vu au début de la console JavaScript : tout comme des fonctions ActiveX exposées en C++, les fonctions VBScript exposées à JavaScript sont détectées comme étant d'un type inconnu (unknown). Il n'est pas possible d'assigner ces fonctions à des variables, d'obtenir leur nom ou de faire n'importe quelle opération à laquelle on pourrait s'attendre à des objets classiques, car rien n'est implémenté sur elles, et il nous faut aussi gérer le fait que quand elles ne sont pas définies, elles seront undefined. Le seul moyen est donc d'utiliser typeof, qui va inspecter le type des variables sans essayer d'appeler une méthode interne à l'API Active Scripting qu'elles n'implémentent pas. On va également préfixer les noms des fonctions par window. pour éviter une ReferenceError si la variable n'est pas définie : alors qu'une variable seule comme ça causera une erreur si elle n'existe pas, accéder à une propriété indéfinie de l'objet window renverra un undefined, qui aura au moins un type différent de unknown.

function supportsVB () {
  return typeof window.VBSEval === 'unknown' && typeof window.VBSExecute === 'unknown'
}

On peut ensuite ajouter un petit bout de code pour désactiver les options de langue VBScript lorsque VBScript n'est pas disponible. On doit aussi appeler cette fonction pour vérifier que VBScript est pris en charge avant d'essayer de déclencher la détection d'assistant Web.

if (!supportsVB()) {
  for (var i = 0; i < languageSelect.children.length; i++) {
    if (languageSelect.children[i].value.slice(0, 3) === 'vbs') {
      languageSelect.children[i].disabled = true;
      languageSelect.children[i].title = 'VBScript is not supported in this browser.';
    }
  }
}

// ...

if (supportsVB() && InWebWizard()) {
  // ...
}

Un détail d'affichage

On utilise sur notre zone de texte un attribut placeholder qui permet d'afficher du texte lorsque rien n'a été écrit. Cet attribut n'est pris en charge qu'à partir d'Internet Explorer 10, et la zone de texte est donc vide sous Windows XP. Dans un navigateur plus récent, ce texte peut être affiché, et il invite à saisir du code JavaScript. Il affiche aussi cette invitation quand on sélectionne du VBScript, en disant toujours JavaScript. On va résoudre ça en ajoutant un nouveau switch pour choisir un texte à afficher :

languageSelect.onchange = function () {
  switch (languageSelect.value) {
    case 'js':
      languageSelect.setAttribute('placeholder', 'Type some JavaScript to run and press Shift+Enter');
      break;
    case 'vbs-eval':
      languageSelect.setAttribute('placeholder', 'Type a VBScript expression to evaluate and press Shift+Enter');
      break;
    case 'vbs-execute':
      languageSelect.setAttribute('placeholder', 'Type some VBScript to run and press Shift+Enter');
      break;
    default:
      languageSelect.setAttribute('placeholder', 'Type the name of an demonic entity to summon and press Shift+Enter');
      break;
  }
}

Je me demande si l'entité démonique n'est pas juste Internet Explorer lui-même.

Conclusion

Avec cet article, on conclut enfin la construction d'un interpréteur pour JavaScript et VBScript qui nous permettra d'aller explorer une ribambelle d'endroits où les ignominies du World Wide Web se sont glissées dans Windows XP. Ces explorations se feront dans des articles qui n'apparaîtront pas forcément tous en même temps, car je découvre parfois ces endroits de façon un peu hasardeuse pendant que j'explore des projets radicalement différents, et je suis encore loin d'avoir vraiment exploité toutes les capacités des API que je trouve pour bricoler des choses de plus en plus bizarres. Une lubie de plus parmi tant d'autres dans mes projets...

Est-ce qu'il sera désormais nécessaire de m'appeler le Seigneur des Consoles ?


Commentaires

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