Spreadsheet-engines evalueren argumenten voordat ze de functie aanroepen die ze ontvangt. Een cel met =WEBSERVICE(AUDIT_TOKEN()) draait dus eerst AUDIT_TOKEN, en als de hosttoepassing AUDIT_TOKEN beantwoordt via een user-defined function handler, dan heeft die handler zijn logregel al weggeschreven, zijn teller al opgehoogd of zijn verbinding al geopend tegen de tijd dat iemand naar het woord WEBSERVICE kijkt. Een blokkadelijst in de handler is te laat. Een lijst die de buitenste naam pas inspecteert als de argumenten terug zijn, ook. De weigering moet op de buitenste naam worden beslist, voordat er ook maar een argument wordt aangeraakt en voordat code die de host schreef een beurt krijgt
De omgeving waarin dit bijt is alledaags. Een service accepteert geüploade XLS- of XLSX-bestanden, rekent ze op de server door en leest enkele totalen terug. Jaren geleden registreerde iemand een user-function handler voor twee zakelijke functies, en onderweg kreeg die een catch-all-tak die elke onbekende naam doorstuurt naar een plugintabel. Niemand in het team typte ooit =WEBSERVICE(...) in een cel. De uploader wel. Die formule intact laten door open, herberekening en opslag is een bestandsgetrouwheidsklus; hem bij hostcode laten komen die sockets kan openen of bestanden kan schrijven is een autorisatiebeslissing, en die twee hebben verschillende eigenaren nodig
Hoe een bewaarde formule je Delphi-callbacks bereikt
In de HotXLS-formule-engine belandt elke aanroep die de calculator niet native kan evalueren in één gedeelde dispatchketen, en gevaarlijke namen komen daar via twee wegen binnen. De ene weg zijn ingebouwde functies die de parser herkent maar waarvoor de calculator geen implementatie heeft: CALL en REGISTER.ID zijn echte Excel-functienamen, dus ze parsen netjes en belanden dan in dezelfde fallback als jouw DISCOUNT of REGIONRATE. De andere weg zijn namen die de parser helemaal niet kent, zoals WEBSERVICE of DDE, die een gelijknamige entry in een registry kunnen matchen of door een catch-all-eventhandler kunnen worden opgeëist. De mechanica van die fallback staat beschreven in hoe HotXLS custom functions oplost via OnUserFunction
De volgorde van de keten bepaalt wat 'onbekend' betekent. Een naam gaat eerst naar de LAMBDA-mechaniek, vóór alles anders: een closure die door LET is gebonden, of een gedefinieerde naam waarvan de formule een LAMBDA is, allebei beschreven in de closure-ondersteuning in de HotXLS-formule-engine. Daarna komen werkboeklokale functies die met RegisterUserFunction zijn geregistreerd, gevolgd door procesbrede functies uit de klassemethode TXLSWorkbook.RegisterGlobalUserFunction, en als laatste de events. OnUserFunctionEx draait als hij is toegekend; OnUserFunction draait alleen als OnUserFunctionEx er niet is. Een echt onbekende naam wordt #NAME? zodra elke fase heeft geweigerd, terwijl een herkende maar niet-geïmplementeerde ingebouwde functie een unsupported function meldt
Elke fase na de LAMBDA-lookup geeft de controle aan code die jij schreef. Daarom zet HotXLS zijn veiligheidscontrole vóór elke fase die bij jouw code komt in plaats van ergens in één ervan, en daarom zit de controle in de calculator zelf in plaats van in de drie bestandslezers. Een formule geladen uit Classic BIFF-tokens, uit XLSX-tekst of uit een ODS msoxl:-formule eindigt als dezelfde geparsede expressie, dus één controle dekt alle drie de formaten en een geconverteerd werkboek gedraagt zich hetzelfde als het origineel
Welke formulenamen weigert HotXLS standaard?
HotXLS weigert een vaste set van 20 namen tenzij callbacks uitdrukkelijk zijn toegestaan: 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 en FILE.DELETE. In Excel of zijn macrotaal zijn dat de namen die native code laden, het net opzoeken, met andere processen praten of het bestandssysteem aanraken. Vóór de vergelijking knipt de engine witruimte eraf, zet hij de naam in hoofdletters en stript hij één _XLFN.- of _XLWS.-prefix. Een bestand geschreven door een nieuwere Excel-build die _xlfn.webservice bewaart, wordt precies zo gepakt als de kale schrijfwijze
De match is exact, en dat snijdt twee kanten op. Een handler die je als MYWEBSERVICE registreert, blijft onaangetast. Een eigen UDF die toevallig OPEN of RUN heet, wordt standaard geweigerd, en de verleidelijke oplossing is AllowUnsafeFormulaCallbacks op het werkboek op True zetten zodat die ene naam weer werkt. Hernoem de functie liever. Het hele werkboek inschakelen om één eigen naam te redden, zet ook CALL, DDE en FILE.DELETE weer aan voor elke formule in dat bestand, inclusief formules die jij niet schreef
De deny-set hekelt ook niets van je eigen handlers af. Als een catch-all-tak willekeurige pluginnamen uitvoert, stopt de gate de 20 bekende gevaarlijke namen en laat hij alles anders door naar jouw code. Het duurzame antwoord is stoppen met catch-all-handlers schrijven. Elke functie op naam registreren met RegisterUserFunction maakt van de registry zelf een allowlist, en het houdt de formule-invoercontrole betekenisvol: zodra OnUserFunction of OnUserFunctionEx is toegekend, kan de engine niet weten welke namen het event zal opeisen, dus stopt de invoervalidatie er helemaal met onbekende functienamen te markeren
procedure TImportService.WireFunctions;
begin
// Eén registratie per naam: de registry is de allowlist
FBook.RegisterUserFunction('DISCOUNT', HandleDiscount);
FBook.RegisterUserFunction('REGIONRATE', HandleRegionRate);
// Laat beide events leeg zodat onbekende namen bij de invoer
// nog worden geweigerd en bij evaluatie als #NAME? eindigen
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 is een variantarray op nulgebaseerde index van al geëvalueerde argumenten
if VarIsArray(Args) and (VarArrayHighBound(Args, 1) = 1) then
begin
Value := Args[0] * (1 - Args[1]);
Handled := True;
end;
end;
Waarom moet de gate vóór de argumentevaluatie draaien?
HotXLS controleert eerst de functienaam en weigert de aanroep voordat hij de argumentarray bouwt, voordat hij een resolver of een van beide registries raadpleegt, en zelfs voordat hij merkt dat er helemaal geen handler is toegekend. Die volgorde is wat de geneste formule uit de opening verslaat: de buitenste WEBSERVICE wordt geweigerd terwijl AUDIT_TOKEN nog een ongeëvalueerde childnode is, dus de hosthandler draait nooit. Het laatste deel van de volgorde doet ertoe voor logging. Een werkboek zonder handlers en een werkboek met vijf rapporteren dezelfde status voor =WEBSERVICE(...), dus een weigeringslog verandert niet van betekenis zodra iemand later een handler koppelt
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'); // side-effect in hostcode
Value := 'token-42';
Handled := True;
end;
end;
procedure TImportService.ProbeNestedCall;
var
Sheet: TXLSWorksheet;
Eval: TXLSFormulaEvaluationResult;
begin
FBook.OnUserFunction := HandleUdf;
Sheet := FBook.Sheets[1]; // worksheetindexen beginnen bij 1
Eval := Sheet.EvaluateFormulaAt(1, 1, '=WEBSERVICE(AUDIT_TOKEN())');
// Verwacht: Eval.Status = xlfeUnsafeFunctionDenied, Eval.Value = Null,
// Eval.Issue.NativeCode = -106 en FAuditLog.Count = 0
end;
De native code -106 is de publieke constante lxErrorUnsafeFunctionDenied, en Eval.Issue.Message luidt 'The formula callback is blocked by the safety policy'. xlfeUnsafeFunctionDenied is het laatste lid van TXLSFormulaEvaluationStatus, na xlfeFailed. Code die statuses met een case en een else-tak voor algemene falen indeelt, boekt een weigering onder 'evaluation failed' tenzij hij de status expliciet benoemt, en voor een uploadpijplijn is dat net de uitkomst die je terugmeldt aan de uploader
Werkboekstandaard versus per-aanroep TXLSFormulaEvaluationOptions
De werkboekvlag stelt de standaard en de per-aanroepoptie heeft het laatste woord. TXLSWorkbook.AllowUnsafeFormulaCallbacks en TXLSXWorkbook.AllowUnsafeFormulaCallbacks beginnen op False en regelen gewone herberekening, Calculate, de drie-argument-EvaluateFormulaAt, EvaluateAt op een evaluatiesjabloon, formules in datavalidatielijsten, read-only views en op XLSX elke worker in de parallelle herberekeningspool. Elke instap die een expliciet TXLSFormulaEvaluationOptions-record neemt, gebruikt Options.AllowUnsafeFormulaCallbacks als het vonnis voor die aanroep en ORt hem niet met de werkboekeigenschap
Die asymmetrie is opzet, en het is de juiste standaard. Een vertrouwde interne klus kan één RTD-lookup autoriseren zonder het hele werkboek om te zetten, en een werkboek dat globaal is ingeschakeld kan nog steeds een gevoelige evaluatie terugdwingen naar weigeren. XLSDefaultFormulaEvaluationOptions geeft de vlag als False terug, dus het record uit die functie bouwen betekent weigeren tenzij je hem met opzet zet. Eén pad negeert de vlag volledig: wanneer Classic-rules voor conditionele opmaak voor rendering worden geëvalueerd, worden callbacks geweigerd wat het werkboek ook zegt
var
Sheet: TXLSWorksheet;
Options: TXLSFormulaEvaluationOptions;
Eval: TXLSFormulaEvaluationResult;
begin
Sheet := FBook.Sheets[1];
// werkboek blijft op slot, één vertrouwde aanroep gaat erdoor
FBook.AllowUnsafeFormulaCallbacks := False;
Options := XLSDefaultFormulaEvaluationOptions;
Options.AllowUnsafeFormulaCallbacks := True;
Eval := Sheet.EvaluateFormulaAt(4, 2, '=WEBSERVICE(B1)', xlfrsA1, Options);
// werkboek ingeschakeld, maar deze evaluatie van geüploade tekst niet
FBook.AllowUnsafeFormulaCallbacks := True;
Options := XLSDefaultFormulaEvaluationOptions; // vlag staat weer op False
Eval := Sheet.EvaluateFormulaAt(4, 2, UploadedFormula, xlfrsA1, Options);
if Eval.Status = xlfeUnsafeFunctionDenied then
LogRejected(Eval.Issue.Message); // LogRejected is je eigen routine
end;
Het wijzigen van de werkboekeigenschap markeert ook elke formule in de afhankelijkheidsgraf als vuil, bij beide werkboekklassen. Zonder dat zou een waarde die is gecached terwijl callbacks waren toegestaan, nog worden geleverd nadat ze zijn ingetrokken, of zou een gecachte weigering een inschakeling overleven. De eigenschap op de waarde zetten die hij al heeft, is een no-op en laat de cache met rust, dus het omschakelen in een lus kost alleen een volledige herberekening als de waarde echt verandert
Wat gebeurt er bij het opslaan met onbekende en onveilige formuletekst?
Een formule bewaren en hem draaien zijn twee aparte vragen, en het invoerbeleid beantwoordt alleen de eerste. FormulaEntryPolicy op beide werkboekklassen draagt UnknownFunctionMode en UnknownNameMode, allebei standaard xlfusmReject. Een formule met een onbekende aanroep toekennen via de gewone Formula-eigenschap gooit een EArgumentException met 'Formula entry rejected: unknown function' voordat celwaarde, formulecache of afhankelijkheden veranderen. ValidateFormulaEntry geeft dezelfde beslissing terug als record, zonder side-effects. Vertrouwde paden zoals een bestand laden, kopiëren en formaatconversie gaan niet door het gebruikersinvoerbeleid, want een strenge standaard mag nooit symbolen weigeren die al in een bestand zitten dat je alleen maar opent
var
Sheet: TXLSWorksheet;
Policy: TXLSFormulaEntryPolicy;
Check: TXLSFormulaEntryValidationResult;
begin
Sheet := FBook.Sheets[1];
Check := Sheet.ValidateFormulaEntry(3, 1, '=ACME_RATE(B3)');
// standaardbeleid, geen handler voor ACME_RATE:
// Check.Issue = xlfeiUnknownFunction, Check.Action = xlfeaReject
Policy := FBook.FormulaEntryPolicy;
Policy.UnknownFunctionMode := xlfusmPreserve; // compatibele invoer
FBook.FormulaEntryPolicy := Policy;
Sheet.Cells[3, 1].Formula := '=ACME_RATE(B3)'; // bewaard, niet geautoriseerd
FBook.SaveAs('rates.xls');
end;
Classic BIFF8 heeft geen token voor een willekeurige functienaam, dus HotXLS schrijft een onbekende aanroep zoals Excel add-in-functies schrijft. De formule krijgt een PtgNameX-token ($59) waarvan de XTI-entry wijst naar de add-in-SUPBOOK met beide sheetindexen op $FFFE, gevolgd door de argumenttokens en een PtgFuncVar met functienummer 255 en een argumenttelling die de naamruimte meetelt. De onderliggende ExternName-body is zes nulbytes, een length-byte en de Unicode-vlag, de UTF-16-functienaam, daarna een formule van twee bytes $1C $17, een PtgErr met #REF!. De writer accepteert namen van 1 tot 255 tekens met hooguit 29 argumenten, en hij richt zich alleen op BIFF8; een BIFF5-opslag kan de aanroep niet dragen. Hoe deze add-in-SUPBOOK-entries naast externe werkboeklinks zitten, wordt uitgelegd in de SUPBOOK- en XTI-classificatieregels voor BIFF externe links
XLSX bewaart de rauwe functietekst en ODS bewaart zijn msoxl:-formule. In alle drie de formaten gaat een bestand dat =WEBSERVICE(...) opslaat, weer open met de naam intact, en zijn standaardevaluatie geeft nog steeds xlfeUnsafeFunctionDenied terug zonder de host aan te roepen. Opslaan promoveert nooit de rechten van een formule: dezelfde tekst die vóór de round trip werd geweigerd, wordt erna geweigerd
Voor een server die werkboeken doorrekent die hij niet zelf schreef, is de opzet die standhoudt kort: laat AllowUnsafeFormulaCallbacks op False staan, registreer functies op naam in plaats van een catch-all-event toe te kennen, en geef per-aanroepopties alleen door waar de formuletekst uit je eigen code komt. De callback-, invoerbeleid- en evaluatiereferentie wordt geleverd bij de documentatie van het HotXLS Delphi spreadsheet component