lucidiot's cybrecluster

Une console VBScript pour Internet Explorer 6, partie 2

Lucidiot Informatique 2022-10-02
Toujours plus de code dans un langage qui fait peur.


On a précédemment posé quelques bases pour le développement d'une console VBScript similaire à la console JavaScript pour jouer avec Internet Explorer 6, 7 et 8. Dans cet article, on va achever cette console, en implémentant le cœur de son fonctionnement, c'est-à-dire l'exécution du code, puis en rajoutant les mêmes outils pratiques que l'autre console comme un raccourci clavier et la gestion du texte sélectionné, et enfin en prenant correctement en charge les assistants Web par lesquels tout ce bazar a commencé.

Exécution du code

Quand j'avais travaillé sur l'exécution de code en JavaScript, j'avais lu une longue explication sur les alternatives à eval(), avant de décider de complètement l'ignorer. Dans VBScript, il existe aussi une expression Eval, mais elle est a un peu plus de spécificités et j'ai dû faire attention à ce que je fais pour de vrai : elle est aussi accompagnée des instructions Execute et ExecuteGlobal, qui peuvent aussi exécuter du code, mais différemment.

Eval s'attend à une expression qui renvoie systématiquement une chaîne de caractères ou un nombre, et rien d'autre. Appeler Eval("Warn \"Something\"") ne va pas fonctionner, car la procédure Warn ne va rien renvoyer. Pour pouvoir exécuter n'importe quel code, on doit donc utiliser soit Execute, soit ExecuteGlobal.

Les deux procédures vont exécuter n'importe quel code VBScript, mais avec des règles différentes : Execute va exécuter le code dans le contexte de la procédure actuelle, donc avec les variables locales disponibles. Cependant, si le code qui est exécuté déclare une nouvelle procédure ou fonction, cette procédure ne sera disponible que dans le contexte local de la procédure qui a lancé Execute, un concept qui est normalement impossible ailleurs dans VBScript (on ne peut pas imbriquer une fonction dans une fonction). La nouvelle procédure n'aura pour autant pas accès aux variables de la procédure parente, puisque les procédures sont censées toutes être dans un contexte global. C'est donc un environnement très étrange.

ExecuteGlobal va quant à elle fonctionner comme Execute mais directement dans un contexte global, comme si on avait appelé Execute dans le corps principal du script et pas dans la procédure. Ça a l'avantage de cacher complètement le contexte d'exécution local, mais l'inconvénient que toutes les procédures ou variables déclarées dans ce code seront disponibles dans le contexte global, donc qu'il est possible de modifier tout et n'importe quoi. Si on lance Execute dans un contexte global et pas dans une fonction ou procédure, on se retrouvera avec le même comportement que ExecuteGlobal.

Dans le cas qui nous intéresse, j'ai choisi d'utiliser Execute ; il sera certes possible de modifier les variables utilisées par la procédure Run, mais c'est un peu de toute façon se tirer une balle dans le pied. En plus, tout ce système n'est conçu que pour fournir une console pour des développeurs, donc quoi qu'on fasse, c'est se tirer des balles uniquement dans ses propres pieds. Tant pis si vous vous faites amputer.

Private Sub Run
    Dim code
    code = input.value
    If IsEmpty(Trim(code)) Then Exit Sub
    ConsoleLog code, "lightblue"
    Err.Clear
    Execute "On Error Resume Next" & vbCrLf & code
    If Err.Number <> 0 Then
        Error "Error " & Err.Number & " at " & Err.Source & ": " & Err.Description
    End If
End Sub

Le dernier aspect intéressant ici est la gestion des erreurs : en VBScript, il n'y a pas de concept de try ou de catch. On dispose de deux possibilités, toutes mauvaises, dans une syntaxe héritée de VB6 : On Error GoTo … et On Error Resume Next.

On Error GoTo … permet de spécifier une étiquette, c'est-à-dire un point dans le code où on veut se « téléporter ». On peut aussi indiquer directement un numéro de ligne, ce qui est encore plus moche. Dans VBScript, il n'y a pas de notions d'étiquettes et les numéros de ligne ne sont pas clairement définis (est-ce les lignes dans la page HTML ou dans le script lui-même ?), donc on ne dispose que de On Error GoTo 0, qui a pour effet de désactiver complètement la gestion des erreurs. Autrement dit, toute erreur est systématiquement fatale.

La seule alternative qu'il nous reste donc est d'utiliser On Error Resume Next, une des instructions les plus tristement célèbres de Visual Basic : en cas d'erreur, le code continue à la ligne suivante. Autrement dit, les erreurs peuvent être entièrement ignorées. Pour détecter si une erreur s'est produite, on utilise Err.Number : si l'objet global Err a un code d'erreur différent de zéro, on sait qu'une erreur s'est produite. On ajoute donc l'instruction On Error Resume Next tout en haut de notre script pour l'activer :

Option Explicit
On Error Resume Next

On pourra ensuite gérer les erreurs qui surviennent en utilisant Execute en commençant d'abord par réinitialiser l'erreur avant d'exécuter le code pour effacer toute précédente erreur avec la procédure Err.Clear. On vérifie ensuite après l'exécution du code s'il y a une erreur : s'il y en a une, on affichera dans la console tout ce qu'on sait sur l'erreur, c'est-à-dire le code d'erreur, l'origine de l'erreur et sa description.

Toutes les erreurs ne seront pas capturées avec cette méthode. On peut augmenter nos chances en rajoutant au morceau de code exécuté dans Execute un préfixe qui forcera un On Error Resume Next à l'intérieur même du contexte d'exécution. Cela permet de détecter notamment des erreurs de syntaxe, mais pas toutes ; il y a des erreurs qu'il sera tout simplement impossible de capturer. Visual Basic, le vrai, et VBA seraient capables de gérer ces exceptions, mais pas VBScript. Tant pis, on aura fait de notre mieux !

Raccourci clavier

Pour gérer le même raccourci Shift + Entrée que dans notre console JavaScript, on ajoute un gestionnaire d'événement qui fait la même chose que dans l'autre console :

Private Sub InputKeyPress
    If window.event.keyCode = 13 And window.event.shiftKey Then
        event.cancelBubble = True
        event.returnValue = False
        Run
    End If
End Sub

Sub window_onload
    '...
    input.onkeypress = GetRef("InputKeyPress")
End Sub

Contrairement à la console JavaScript, on ne peut même pas essayer d'appeler event.preventDefault ou event.stopPropagation quand ils existent, ou d'utiliser un argument event au lieu de la variable globale window.event, car VBScript n'est pas aussi flexible concernant les types que JavaScript et ne nous permet donc pas facilement de tester si un objet a un attribut, ou n'autorise pas des procédures ou des fonctions à manquer d'arguments. Tant pis, on utilisera juste les propriétés dépréciées.

Exécuter le texte sélectionné

Pour gérer le texte sélectionné, on va juste traduire ce qu'on a fait pour JavaScript mais en VBScript.

Private Function RemoveCr(ByVal text)
    RemoveCr = Replace(text, vbCrLf, vbLf)
End Function

Private Function GetSelectedText(element)
    Dim range
    Set range = document.selection.createRange()
    If IsNull(range) Or range.parentElement() <> element Then
        GetSelectedText = ""
        Exit Function
    End If

    Dim normalizedValue, inputRange, endRange
    normalizedValue = RemoveCr(element.value)
    Set inputRange = element.createTextRange()
    inputRange.moveToBookmark range.getBookmark()
    Set endRange = element.createTextRange()
    endRange.collapse False

    'Selection begins at the end of the input, so it's empty
    If inputRange.compareEndPoints("StartToEnd", endRange) > -1 Then
        GetSelectedText = ""
        Exit Function
    End If

    Dim selectionStart, selectionEnd
    selectionStart = -inputRange.moveStart("character", -Len(element.value))
    selectionStart = selectionStart + UBound(Split(Left(normalizedValue, selectionStart), vbLf)) + 1
    If selectionStart <= 0 Then selectionStart = 1

    selectionEnd = Len(normalizedValue)
    If inputRange.compareEndPoints("EndToEnd", endRange) < 0 Then
        selectionEnd = -inputRange.moveEnd("character", -Len(element.value))
        selectionEnd = selectionEnd + UBound(Split(Left(normalizedValue, selectionEnd), vbLf))
    End If

    GetSelectedText = Mid(normalizedValue, selectionStart, selectionEnd - selectionStart + 1)
End Function

Private Sub Run
    Dim code
    code = GetSelectedText(input)
    If code = "" Then code = RemoveCr(input.value)
    If Trim(code) = "" Then Exit Sub
    '...
End Sub

Il y a moult choses à noter ici. D'abord, pour retirer les caractères de retour chariot, on utilise Replace, qui n'utilise pas des expressions régulières mais permet de remplacer n'importe quelle séquence de caractères par une autre. On utilise vbCrLf et vbLf, deux des nombreuses constantes étranges qu'on retrouve dans quasiment toutes les variantes de Visual Basic, et qui sont juste des raccourcis pour les caractères qu'on cherche.

Ensuite, dans notre fonction pour récupérer le texte sélectionné, on ne gère pas les attributs plus modernes comme selectionStart ou selectionEnd, car il n'y a pas de moyen simple de détecter si une propriété d'un objet existe en VBScript, contrairement à JavaScript où on peut juste se reposer sur undefined. On utilise l'instruction Exit Function à plusieurs reprises pour pouvoir effectuer des retours de fonction plus tôt que dans son exécution normale ; utiliser seulement GetSelectedText = ... ne suffira pas.

Enfin, la méthode Left permet de récupérer un certain nombre de premiers caractères d'une chaîne, Mid permet de récupérer un certain nombre de caractères à partir d'un index de départ, et UBound permet de déterminer le nombre d'entrées d'un tableau. On doit interdire les valeurs négatives pour l'index de début de sélection, car contrairement à la méthode .substring() de JavaScript, Mid ne considère pas des indices négatifs comme étant équivalents au début de la chaîne. J'ai passé beaucoup de temps à bricoler pour réussir à gérer les indices qui commencent parfois à 1 au lieu de 0, mais pas toujours...

Gestion de IE 7

Le bug qu'on rencontre sous Internet Explorer 7 est exactement le même dans le cas de cette console, donc on va restaurer exactement le même fonctionnement. Pour avoir un code plus compréhensible, j'ai mis la détection d'IE 7 dans une fonction, et je m'en sers au chargement pour modifier la hauteur et la marge comme avant :

Private Function InIE7()
    InIE7 = InStr(navigator.userAgent, "MSIE 7.0")
End Function

Sub window_onload
    '...
    If InIE7() Then
        input.style.height = document.documentElement.clientHeight - 7 & "px"
        output.style.marginLeft = "7px"
    End If
End Sub

Gestion de console existante

Dans Internet Explorer 8 et ultérieur, l'objet console est bien disponible dans le contexte d'exécution VBScript : IsEmpty(console), qui vérifie si une variable est définie ou non, renvoie True, et TypeName(console) renvoie Object, ce qui est cohérent. Cela dit, il ne me semble pas possible d'appeler console.info ou console.log du tout. C'est encore pire pour console.error, puisque Error est un mot-clé réservé et cause de la confusion.

Par conséquent, contrairement à la console JavaScript, on ne prendra pas en charge avec VBScript la gestion d'une console de développeurs existante. En plus, la console d'Internet Explorer ne permet d'exécuter que du JavaScript et pas du VBScript.

Gestion des assistants Web

J'avais indiqué précédemment que VBScript pouvait avoir des avantages pour notre exploration des objets ActiveX. Notre avantage se présente déjà ici pour la détection d'un contexte d'assistant Web. Alors qu'on avait des types unknown pour les objets ActiveX et des types object pour les fonctions en JavaScript, on peut en VBScript simplement demander le type de l'objet ActiveX et obtenir le vrai nom de son type. On va pouvoir utiliser ça pour détecter immédiatement l'interface spécifique à l'assistant Web :

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

Sub window_onload
    '...
    If InWebWizard() Then
        window.external.SetHeaderText "VBScript Console", "Explore this wizard's VBScript environment."
        window.external.SetWizardButtons True, False, False
    End If
End Sub

Sub OnBack
    If InWebWizard() Then window.external.FinalBack
End Sub

On n'a cette fois pas besoin de tester si chaque méthode est présente, car on a déjà vérifié que toute l'interface est disponible de façon plus précise. C'est quasiment le seul avantage qu'on a de base avec VBScript !

Conclusion

J'avais à l'origine prévu de n'écrire qu'un seul article expliquant le développement des deux consoles JS et VBS, mais en commençant l'écriture de la partie sur JavaScript, c'était déjà devenu suffisamment long pour découper au moins en deux, un article pour JS et un article pour VBS. Finalement, je me suis retrouvé à couper en quatre, puis à avoir d'autres idées pour d'autres articles qui pourront suivre, en plus de la série que je comptais faire.

On pourra plus tard accéder à des contrôles ActiveX installés sur l'ordinateur, ou de notre fabrication, qu'on pourra utiliser pour débloquer des fonctionnalités qu'on aurait normalement à disposition qu'en VB6 ou en C++. On pourra notamment essayer d'en utiliser pour explorer les objets de l'API Win32 plus facilement qu'on le ferait avec du code JS ou VBS.

On pourra déjà essayer de placer les consoles partout où on peut les mettre. Mais avant ça, on pourra juste éviter d'avoir deux consoles séparées, et n'en avoir qu'une seule pour profiter plus facilement des deux langages simultanément...


Commentaires

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