Les moteurs de tableur évaluent les arguments avant d'appeler la fonction qui les reçoit. Une cellule portant =WEBSERVICE(AUDIT_TOKEN()) exécute donc AUDIT_TOKEN d'abord, et si l'application hôte répond à AUDIT_TOKEN via un handler de fonctions utilisateur, ce handler a déjà écrit sa ligne de journal, incrémenté son compteur ou ouvert sa connexion au moment où quelque chose regarde le mot WEBSERVICE. Une liste noire dans le handler arrive trop tard. Il en va de même pour une liste qui inspecte le nom externe après le retour des arguments. Le refus doit être décidé sur le nom externe, avant que le moindre argument ne soit touché et avant que le moindre code écrit par l'hôte n'ait son tour
Le cadre où ça mord est banal. Un service accepte des fichiers XLS ou XLSX téléversés, les recalcule sur le serveur et relit quelques totaux. Il y a des années, quelqu'un a enregistré un handler de fonctions utilisateur pour deux fonctions métier, et en chemin il a poussé une branche fourre-tout qui transmet tout nom non reconnu à une table de plugins. Personne dans l'équipe n'a jamais tapé =WEBSERVICE(...) dans une cellule. Le téléverseur, si. Préserver cette formule à travers ouverture, recalcul et sauvegarde est un travail de fidélité de fichier ; la laisser atteindre du code hôte capable d'ouvrir des sockets ou d'écrire des fichiers est une décision d'autorisation, et les deux ont besoin de propriétaires différents
Comment une formule préservée atteint vos callbacks Delphi
Dans le moteur de formules HotXLS, tout appel que le calculateur ne peut pas évaluer nativement tombe dans une chaîne de dispatch partagée, et les noms dangereux y arrivent par deux routes. L'une, ce sont les fonctions intégrées que le parseur reconnaît mais pour lesquelles le calculateur n'a pas d'implémentation : CALL et REGISTER.ID sont de vrais noms de fonctions Excel, donc ils s'analysent proprement puis atterrissent dans le même repli que votre DISCOUNT ou REGIONRATE. L'autre route, ce sont les noms que le parseur ne connaît pas du tout, comme WEBSERVICE ou DDE, qui peuvent correspondre à une entrée homonyme dans un registre ou être revendiqués par un handler d'événement fourre-tout. La mécanique de ce repli est couverte dans comment HotXLS résout les fonctions personnalisées via OnUserFunction
L'ordre de la chaîne décide de ce que veut dire « inconnu ». Un nom va à la machinerie LAMBDA avant tout le reste : une closure liée par LET, ou un nom défini dont la formule est une LAMBDA, tous deux décrits dans le support des closures dans le moteur de formules HotXLS. Viennent ensuite les fonctions locales au classeur enregistrées avec RegisterUserFunction, puis les fonctions à l'échelle du processus de la méthode de classe TXLSWorkbook.RegisterGlobalUserFunction, et enfin les événements. OnUserFunctionEx tourne quand il est affecté ; OnUserFunction ne tourne que quand OnUserFunctionEx ne l'est pas. Un nom vraiment inconnu devient #NAME? une fois que chaque étape a décliné, tandis qu'un intégré reconnu mais non implémenté rapporte une fonction non supportée
Chaque étape après la recherche LAMBDA passe le contrôle à du code que vous avez écrit. C'est pourquoi HotXLS place son contrôle de sûreté devant chaque étape qui atteint votre code plutôt qu'à l'intérieur de l'une d'elles, et pourquoi le contrôle se trouve dans le calculateur lui-même au lieu des trois lecteurs de fichiers. Une formule chargée de tokens BIFF classiques, de texte XLSX ou d'une formule ODS msoxl: finit dans la même expression analysée, donc un seul contrôle couvre les trois formats et un classeur converti se comporte comme l'original
Quels noms de formules HotXLS interdit-il par défaut ?
HotXLS interdit un ensemble fixe de 20 noms tant que les callbacks ne sont pas explicitement autorisés : DDE, CALL, REGISTER, REGISTER.ID, WEBSERVICE, RTD, SQL.REQUEST, EXEC, RUN, CREATE.OBJECT, APP.ACTIVATE, SEND.KEYS, OPEN, SAVE, SAVE.AS, FOPEN, FWRITE, FWRITELN, FCLOSE et FILE.DELETE. Dans Excel ou son langage macro, ce sont les noms qui chargent du code natif, atteignent le réseau, parlent à d'autres processus ou touchent le système de fichiers. Avant de comparer, le moteur rogne les blancs autour, met le nom en majuscules et retire un préfixe unique _XLFN. ou _XLWS.. Un fichier écrit par une build Excel plus récente qui stocke _xlfn.webservice est attrapé exactement comme l'orthographe nue
La correspondance est exacte, ce qui coupe dans les deux sens. Un handler que vous enregistrez comme MYWEBSERVICE n'est pas concerné. Une UDF interne qui s'appellerait justement OPEN ou RUN est interdite par défaut, et la réparation tentante consiste à passer AllowUnsafeFormulaCallbacks à True sur le classeur pour que ce nom refonctionne. Renommez la fonction à la place. Inscire tout le classeur dans l'option pour sauver un seul nom interne réactive aussi CALL, DDE et FILE.DELETE pour chaque formule de ce fichier, y compris les formules que vous n'avez pas écrites
L'ensemble d'interdiction ne clôture pas non plus vos propres handlers. Si une branche fourre-tout exécute des noms de plugins arbitraires, le verrou arrête les 20 noms dangereux bien connus et laisse tout le reste passer jusqu'à votre code. La réponse durable est d'arrêter d'écrire des handlers fourre-tout. Enregistrer chaque fonction par nom avec RegisterUserFunction fait du registre lui-même une liste blanche, et ça garde le contrôle d'entrée des formules porteur de sens : quand OnUserFunction ou OnUserFunctionEx est affecté, le moteur ne peut pas savoir quels noms l'événement revendiquera, donc la validation d'entrée cesse carrément de signaler les noms de fonctions inconnus
procedure TImportService.WireFunctions;
begin
// Un enregistrement par nom : le registre est la liste blanche
FBook.RegisterUserFunction('DISCOUNT', HandleDiscount);
FBook.RegisterUserFunction('REGIONRATE', HandleRegionRate);
// Laissez les deux événements non affectés pour que les noms inconnus
// soient toujours refusés à l'entrée et finissent en #NAME? à l'évaluation
FBook.OnUserFunction := nil;
FBook.OnUserFunctionEx := nil;
end;
procedure TImportService.HandleDiscount(Sender: TObject;
const FunctionName: WideString; const Args: Variant;
var Value: Variant; var Handled: Boolean);
begin
// Args est un tableau de variants à base zéro d'arguments déjà évalués
if VarIsArray(Args) and (VarArrayHighBound(Args, 1) = 1) then
begin
Value := Args[0] * (1 - Args[1]);
Handled := True;
end;
end;
Pourquoi le verrou doit-il passer avant l'évaluation des arguments ?
HotXLS vérifie le nom de fonction d'abord et refuse l'appel avant de construire le tableau d'arguments, avant de consulter un résolveur ou l'un des registres, et même avant de remarquer qu'aucun handler n'est affecté du tout. Cet ordre est ce qui bat la formule imbriquée de l'ouverture : le WEBSERVICE externe est refusé tandis que AUDIT_TOKEN est encore un nœud enfant non évalué, donc le handler hôte ne tourne jamais. La dernière partie de l'ordre compte pour la journalisation. Un classeur sans handlers et un classeur avec cinq rapportent le même statut pour =WEBSERVICE(...), donc un journal de refus ne change pas de sens quand quelqu'un branche plus tard un handler
procedure TImportService.HandleUdf(Sender: TObject;
const FunctionName: WideString; const Args: Variant;
var Value: Variant; var Handled: Boolean);
begin
if SameText(FunctionName, 'AUDIT_TOKEN') then
begin
FAuditLog.Add('AUDIT_TOKEN evaluated'); // effet de bord dans le code hôte
Value := 'token-42';
Handled := True;
end;
end;
procedure TImportService.ProbeNestedCall;
var
Sheet: TXLSWorksheet;
Eval: TXLSFormulaEvaluationResult;
begin
FBook.OnUserFunction := HandleUdf;
Sheet := FBook.Sheets[1]; // les index de feuille commencent à 1
Eval := Sheet.EvaluateFormulaAt(1, 1, '=WEBSERVICE(AUDIT_TOKEN())');
// Attendu : Eval.Status = xlfeUnsafeFunctionDenied, Eval.Value = Null,
// Eval.Issue.NativeCode = -106 et FAuditLog.Count = 0
end;
Le code natif -106 est la constante publique lxErrorUnsafeFunctionDenied, et Eval.Issue.Message lit « The formula callback is blocked by the safety policy ». xlfeUnsafeFunctionDenied est le dernier membre de TXLSFormulaEvaluationStatus, après xlfeFailed. Du code qui mappe les statuts avec un case et une branche else pour l'échec générique classera un refus sous « échec d'évaluation » à moins de nommer le statut explicitement, et pour un pipeline de téléversement c'est la seule issue qui vaille d'être remontée au téléverseur
Défaut du classeur contre TXLSFormulaEvaluationOptions par appel
Le drapeau du classeur pose la valeur par défaut et l'option par appel a le dernier mot. TXLSWorkbook.AllowUnsafeFormulaCallbacks et TXLSXWorkbook.AllowUnsafeFormulaCallbacks démarrent à False et gouvernent le recalcul ordinaire, Calculate, le EvaluateFormulaAt à trois arguments, EvaluateAt sur un modèle d'évaluation, les formules de listes de validation de données, les vues en lecture seule et, sur XLSX, chaque worker du pool de recalcul parallèle. Tout point d'entrée qui prend un enregistrement TXLSFormulaEvaluationOptions explicite utilise Options.AllowUnsafeFormulaCallbacks comme verdict pour cet appel et ne le combine pas avec la propriété du classeur
Cette asymétrie est délibérée, et c'est la bonne valeur par défaut. Un travail interne de confiance peut autoriser une recherche RTD sans basculer tout le classeur, et un classeur globalement inscrit dans l'option peut quand même forcer une évaluation sensible au refus. XLSDefaultFormulaEvaluationOptions renvoie le drapeau à False, donc construire l'enregistrement depuis cette fonction veut dire refus tant que vous ne le posez pas à dessein. Un chemin ignore complètement le drapeau : quand les règles classiques de mise en forme conditionnelle sont évaluées pour le rendu, les callbacks sont refusés quoi que dise le classeur
var
Sheet: TXLSWorksheet;
Options: TXLSFormulaEvaluationOptions;
Eval: TXLSFormulaEvaluationResult;
begin
Sheet := FBook.Sheets[1];
// le classeur reste verrouillé, un seul appel de confiance passe
FBook.AllowUnsafeFormulaCallbacks := False;
Options := XLSDefaultFormulaEvaluationOptions;
Options.AllowUnsafeFormulaCallbacks := True;
Eval := Sheet.EvaluateFormulaAt(4, 2, '=WEBSERVICE(B1)', xlfrsA1, Options);
// classeur inscrit dans l'option, mais cette évaluation de texte téléversé non
FBook.AllowUnsafeFormulaCallbacks := True;
Options := XLSDefaultFormulaEvaluationOptions; // le drapeau repasse à False
Eval := Sheet.EvaluateFormulaAt(4, 2, UploadedFormula, xlfrsA1, Options);
if Eval.Status = xlfeUnsafeFunctionDenied then
LogRejected(Eval.Issue.Message); // LogRejected est votre propre routine
end;
Changer la propriété du classeur marque aussi chaque formule du graphe de dépendances comme sale, sur les deux classes de classeurs. Sans ça, une valeur mise en cache pendant que les callbacks étaient autorisés pourrait être servie après leur révocation, ou un refus en cache pourrait survivre à une inscription dans l'option. Positionner la propriété sur la valeur qu'elle tient déjà est un no-op et laisse le cache tranquille, donc la basculer dans une boucle ne coûte un recalcul complet que quand la valeur change réellement
Que devient le texte des formules inconnues et à risque à la sauvegarde ?
Garder une formule et l'exécuter sont deux questions séparées, et la politique d'entrée ne répond qu'à la première. FormulaEntryPolicy sur chaque classe de classeur porte UnknownFunctionMode et UnknownNameMode, tous deux à xlfusmReject par défaut. Affecter une formule avec un appel inconnu via la propriété Formula ordinaire lève EArgumentException avec « Formula entry rejected: unknown function » avant que la valeur de cellule, le cache de formules ou les dépendances ne changent. ValidateFormulaEntry renvoie la même décision sous forme d'enregistrement, sans effets de bord. Les chemins de confiance comme le chargement d'un fichier, la copie et la conversion de format ne passent pas par la politique d'entrée utilisateur, car une valeur par défaut stricte ne doit jamais rejeter des symboles déjà présents dans un fichier que vous vous contentez d'ouvrir
var
Sheet: TXLSWorksheet;
Policy: TXLSFormulaEntryPolicy;
Check: TXLSFormulaEntryValidationResult;
begin
Sheet := FBook.Sheets[1];
Check := Sheet.ValidateFormulaEntry(3, 1, '=ACME_RATE(B3)');
// politique par défaut, aucun handler pour ACME_RATE :
// Check.Issue = xlfeiUnknownFunction, Check.Action = xlfeaReject
Policy := FBook.FormulaEntryPolicy;
Policy.UnknownFunctionMode := xlfusmPreserve; // entrée pour compatibilité
FBook.FormulaEntryPolicy := Policy;
Sheet.Cells[3, 1].Formula := '=ACME_RATE(B3)'; // stockée, pas autorisée
FBook.SaveAs('rates.xls');
end;
Le BIFF8 classique n'a pas de token pour un nom de fonction arbitraire, donc HotXLS écrit un appel inconnu comme Excel écrit les fonctions complémentaires. La formule reçoit un token PtgNameX ($59) dont l'entrée XTI pointe vers le SUPBOOK complémentaire avec les deux index de feuille à $FFFE, suivie des tokens d'arguments et d'un PtgFuncVar portant le numéro de fonction 255 et un compte d'arguments qui inclut le slot du nom. Le corps ExternName sous-jacent est six octets à zéro, un octet de longueur et le drapeau Unicode, le nom de fonction en UTF-16, puis une formule de deux octets $1C $17, un PtgErr portant #REF!. L'écrivain accepte des noms de 1 à 255 caractères avec au plus 29 arguments, et il ne vise que le BIFF8 ; une sauvegarde BIFF5 ne peut pas porter l'appel. La façon dont ces entrées SUPBOOK complémentaires se placent à côté des liens externes vers classeurs est expliquée dans les règles de classification SUPBOOK et XTI des liens externes BIFF
XLSX garde le texte brut de la fonction et ODS garde sa formule msoxl:. Dans les trois formats, un fichier qui a sauvegardé =WEBSERVICE(...) se rouvre avec le nom intact, et son évaluation par défaut renvoie toujours xlfeUnsafeFunctionDenied sans appeler l'hôte. La sauvegarde n'augmente jamais les permissions d'une formule : le même texte refusé avant le aller-retour est refusé après
Pour un serveur qui recalcule des classeurs qu'il n'a pas écrits, la configuration qui tient la route est courte : laissez AllowUnsafeFormulaCallbacks à False, enregistrez les fonctions par nom au lieu d'affecter un événement fourre-tout, et ne passez des options par appel que là où le texte de formule vient de votre propre code. La référence des callbacks, de la politique d'entrée et de l'évaluation est livrée avec la documentation du HotXLS Delphi spreadsheet component