Articolo tecnico

Eseguire JavaScript AcroForm in Delphi con PDFium Component

Un modulo i cui totali si ricalcolano in Acrobat ma restano congelati nel vostro viewer Delphi non è quasi mai un bug di rendering. PDFium disattiva tutto il JavaScript di documento a meno che l'host non colleghi una piattaforma JS, e PDFium Component collega quella piattaforma automaticamente dalla versione 2.13.2 in poi, così il JavaScript AcroForm, gli script a livello di documento e i calcoli dei campi vengono eseguiti ogni volta che è caricata la DLL abilitata a V8. L'host resta al comando attraverso un insieme di eventi che possono visualizzare, deviare o vietare tutto ciò che lo script prova a fare

Flusso delle azioni di script AcroForm attraverso gli eventi host in PDFium Component: alert, print, mailDoc e submitForm raggiungono un handler che può visualizzare, implementare o vietare ciascuna azione
il componente non esegue di per sé alcun trasporto di rete o MAPI, quindi un host impreparato riduce a nulla un submitForm ostile

Quella singola frase risolve una domanda di supporto che abbiamo visto in varie forme: il mio modulo di ordine calcola i totali di riga in Acrobat ma non nella mia applicazione, app.alert non si attiva mai, il campo data non si formatta da solo. Tutte riconducono allo stesso contratto, e capirlo vale dieci minuti perché lo stesso contratto è anche il vostro confine di sicurezza contro i documenti ostili

Perché i calcoli dei moduli PDF non funzionano in PDFium?

PDFium tratta il JavaScript di documento come strettamente opzionale: se il membro m_pJsPlatform dell'ambiente di compilazione dei moduli è NULL, ogni script del documento viene saltato in silenzio. Nessun errore, nessuna riga di log, nessuna esecuzione parziale. I campi calcolati mantengono il loro ultimo valore memorizzato, app.alert svanisce, gli script di formato non girano mai. Acrobat, che porta sempre con sé il proprio motore JavaScript, esegue tranquillamente lo stesso file, ed è per questo che la discrepanza somiglia tanto a un bug nel vostro codice quando in realtà è un valore predefinito deliberato nella libreria sottostante

PDFium Component aveva un secondo livello storico in questa vicenda. Fino alla versione 2.13.1 il binding collegava m_pJsPlatform soltanto dentro il ramo di inizializzazione XFA, quindi i normali documenti AcroForm, che sono la stragrande maggioranza dei PDF con script, non ottenevano alcuna piattaforma JavaScript nemmeno quando la DLL abilitata a V8 era presente. La versione 2.13.2 ha estratto quel collegamento dalla decisione su XFA: InitializeFormFill ora costruisce la tabella IPDF_JsPlatform ogni volta che V8FeaturesAvailable restituisce True, indipendentemente dal fatto che il documento sia XFA. Il risultato pratico è che gli script a livello di documento, le catene di calcolo dei campi e app.alert ora vengono eseguiti anche nei normali documenti AcroForm

Diagramma del contratto della piattaforma JavaScript di PDFium: un m_pJsPlatform NULL salta in silenzio ogni script mentre PDFium Component collega la piattaforma ogni volta che V8FeaturesAvailable è vero
totali congelati e alert svaniti risalgono al contratto della piattaforma, non a un bug di rendering — non viene mai sollevato alcun errore

Quale DLL V8 serve al JavaScript AcroForm in Delphi?

Il JavaScript AcroForm ha bisogno di un binario PDFium compilato con il motore V8, che PDFium Component carica come pdfium.v8.dll quando il flag globale EnableV8Engine è True prima del primo caricamento della libreria. C'è qui una trappola che vale la pena dire chiaramente: il componente rileva automaticamente i documenti XFA con una scansione preliminare dei marcatori /XFA e attiva EnableV8Engine per voi, ma un normale documento AcroForm con JavaScript non porta alcun marcatore XFA, quindi nessun rilevamento automatico avviene. Se il vostro viewer deve eseguire gli script dei moduli, impostate il flag voi stessi, una volta, prima che venga caricato il primo documento; dopo che una pdfium.dll semplice è stata caricata il processo non può cambiare motore

uses PDFium;

procedure TViewerForm.FormCreate(Sender: TObject);
begin
  // Deve girare prima del primo caricamento della libreria singleton PDFium;
  // una volta che pdfium.dll semplice è nel processo non si può sostituire
  EnableV8Engine := True;
end;

procedure TViewerForm.OpenDocument(const FileName: string);
begin
  FPdf.LoadDocument(FileName);
  if not V8FeaturesAvailable then
    StatusBar.SimpleText :=
      'JavaScript disabled: pdfium.v8.dll not deployed';
end;

V8FeaturesAvailable è la sonda onesta a runtime: riporta se il binario caricato risolve davvero le export dipendenti da V8, così il controllo funziona indipendentemente da quale DLL un installer abbia finito per distribuire. Lo stesso motore e lo stesso flag alimentano anche il rendering XFA dinamico; il contesto su come interagiscono il rilevamento XFA e la selezione automatica di V8 è trattato in rilevare i moduli XFA ed estrarre i pacchetti XFA con PDFium

Eventi host per alert, stampa, posta e invio del modulo

Ogni cosa visibile che uno script fa torna al vostro codice attraverso eventi di TPdf, e ognuno di essi per impostazione predefinita non fa nulla quando non è assegnato. OnJavaScriptAlert riceve messaggio, titolo, tipo di pulsanti e icona da app.alert e vi lascia restituire il risultato del pulsante premuto; OnJavaScriptResponse serve i prompt di input di app.response compreso il flag della password; OnJavaScriptBeep, OnJavaScriptPrint, OnJavaScriptMail e OnJavaScriptSubmitForm espongono le richieste di beep, stampa, posta e invio con i loro insiemi completi di parametri. Un viewer che non assegna nessuno di questi eventi renderizza e calcola comunque correttamente; il documento semplicemente non può aprire finestre di dialogo, stampare o inviare alcunché

procedure TViewerForm.PdfJavaScriptAlert(Sender: TObject;
  const Msg, Title: WString; ButtonType, Icon: Integer;
  var Result_: Integer; var Handled: Boolean);
begin
  // Instrada app.alert nella VCL così appartiene alla nostra finestra
  Result_ := MessageBox(Handle, PWideChar(Msg), PWideChar(Title),
    MB_OK or MB_ICONINFORMATION);
  Handled := True;
end;

procedure TViewerForm.PdfJavaScriptSubmitForm(Sender: TObject;
  const Url: WString; const Data: TBytes);
begin
  // Nulla viene trasmesso se non è questo handler a scegliere di inviarlo
  LogAudit(Format('Document requested form submit to %s (%d bytes)',
    [Url, Length(Data)]));
end;

La divisione dei comportamenti predefiniti conta per il modello di minaccia. OnJavaScriptMail e OnJavaScriptSubmitForm sono di tipo notifica: PDFium Component non compie da solo alcuna azione di rete o MAPI, quindi un documento malevolo che chiama this.submitForm o this.mailDoc contro un host impreparato non ottiene esattamente nulla. L'host deve aderire assegnando un handler e implementando esso stesso il trasporto, il che significa che la decisione di spostare dati fuori dalla macchina è sempre vostra, presa nel vostro codice, con URL e payload alla mano

Come si impedisce a un PDF malevolo di lanciare URL?

Le azioni URI sono l'unico punto in cui il comportamento predefinito storico era attivo anziché passivo, ed è per questo che hanno ricevuto un veto dedicato. Prima della versione 2.13.1 il callback FFI_DoURIActionWithKeyboardModifier eseguiva tramite shell qualunque URI un campo XFA fornisse, senza condizioni, consegnando così a un documento malevolo la possibilità di lanciare URL arbitrari al clic. OnXfaUriAction chiude quella falla: il componente consulta l'evento prima di eseguire, e un handler che imposta Handled a True sopprime completamente il comportamento predefinito di ShellExecuteW. Lasciare l'evento non assegnato conserva il vecchio comportamento per compatibilità, quindi un viewer attento alla sicurezza dovrebbe assegnarlo sempre

procedure TViewerForm.PdfXfaUriAction(Sender: TObject;
  const Uri: WString; Modifiers: Integer; var Handled: Boolean);
begin
  // Vieta tutto ciò che non è https puro; altrimenti il default
  // eseguirebbe ShellExecuteW sull'URI così com'è
  if not SameText(Copy(Uri, 1, 8), 'https://') then
  begin
    LogAudit('Blocked URI action: ' + Uri);
    Handled := True;
  end;
end;

Una lista di schemi consentiti è il minimo; un viewer più severo chiede conferma all'utente o instrada tutto attraverso un proprio componente browser in sandbox. La stessa filosofia di veto si estende a tutto l'insieme di eventi della v2.13.1: OnXfaEmail, OnXfaHttpRequest e OnXfaOpenFile espongono ciascuno i parametri testuali dell'azione richiesta mentre il componente stesso non compie alcun trasporto di rete o di file. Come queste guardie si inseriscano in una strategia più ampia per i documenti non fidati, compreso l'isolamento del rendering, è l'argomento di costruire un'anteprima PDF sicura in Delphi

Diagramma su come fermare i lanci di URL malevoli in Delphi: intercettare l'azione URI, estrarre lo schema, applicare una lista di schemi consentiti, chiedere conferma all'utente o instradare verso un browser in sandbox
una lista di schemi consentiti è l'asticella minima; la stessa filosofia di veto copre OnXfaEmail, OnXfaHttpRequest e OnXfaOpenFile

Scrivere nel campo con focus tramite SetFocusedFormFieldText

TPdf.SetFocusedFormFieldText, aggiunto nella versione 2.13.2, è il compagno in scrittura di FocusedFormFieldText: seleziona il contenuto attuale del campo con focus tramite FORM_SelectAllText e lo sovrascrive attraverso FORM_ReplaceSelection, esattamente come se l'utente avesse digitato la sostituzione. Poiché il testo entra dal percorso di modifica interattiva, tutti gli script di tasto, formato e calcolo collegati al campo si attivano allo stesso modo in cui lo fanno per l'input manuale, il che ne fa la primitiva giusta per la compilazione programmatica in un viewer che tiene vivo il JavaScript. L'attraversamento dei campi e la gestione del focus attorno a esso sono trattati in navigazione fra i campi modulo con PDFium Component

if FPdf.FocusedFormFieldIndex >= 0 then
begin
  if not FPdf.SetFocusedFormFieldText('42.50') then
    ShowMessage('No focused field accepts text on this page');
  // AcroForm: il valore va in /V quando il focus lascia il campo.
  // XFA: la scrittura resta solo nel buffer di modifica in memoria e
  // non sopravvive a un salvataggio
end;

L'asimmetria di persistenza in quel commento è un confine reale, non un avvertimento per pura forma. Per i campi di testo e combo AcroForm il valore modificato viene fissato nella voce /V del campo alla perdita del focus e persiste attraverso il salvataggio. Per i campi di testo XFA la scrittura finisce soltanto nel buffer di modifica in memoria, perché PDFium non espone alcuna API pubblica per riconciliare i valori dei widget nel pacchetto datasets XFA; salvare il documento non porterà con sé la modifica. Se il requisito è una modifica durevole dei dati XFA, modificate l'XML dei datasets stesso invece del widget

Limiti da conoscere prima di rilasciare

Restano due confini onesti. Primo, l'API JavaScript che PDFium implementa è un sottoinsieme funzionante del modello a oggetti di Acrobat, centrato sul nucleo dello scripting dei moduli: accesso ai campi, eventi di calcolo e formato, app.alert e app.response, richieste di stampa, posta e invio. I documenti che si appoggiano a oggetti esotici presenti solo in Acrobat degraderanno, di solito in silenzio, quindi collaudate i moduli reali che i vostri utenti trattano invece di dare per scontata la parità. Secondo, abilitare V8 significa distribuire pdfium.v8.dll, un binario sensibilmente più grande della build semplice; un viewer che non ha mai bisogno di scripting o XFA può legittimamente tenere la DLL più piccola e lasciare m_pJsPlatform non collegato, che è di per sé una postura di sicurezza valida

Lo schema che emerge da tutto questo è piacevolmente noioso: impostate EnableV8Engine all'avvio, assegnate gli eventi di alert e response perché le finestre di dialogo sembrino native, assegnate OnXfaUriAction e registrate in audit gli eventi di posta e invio, e lasciate tutto il resto al comportamento predefinito che non fa nulla finché un requisito non dice altrimenti. Il riferimento completo degli eventi e la superficie API di compilazione dei moduli sono documentati nella pagina di prodotto di PDFium Component