Articolo tecnico

Esecuzione di JavaScript AcroForm in Delphi con PDFium Component

Un modulo i cui totali vengono ricalcolati in Acrobat ma rimangono bloccati nel tuo visualizzatore Delphi non è quasi mai un bug di rendering. PDFium disabilita tutto il JavaScript del documento a meno che l'host non colleghi una piattaforma JS, e il Componente PDFium gestisce tale collegamento automaticamente a partire dalla versione 2.13.2, garantendo che JavaScript AcroForm, script a livello di documento e calcoli dei campi vengano eseguiti ogni volta che la DLL abilitata per V8 viene caricata. L'host mantiene il controllo tramite un set di eventi che possono visualizzare, reindirizzare o bloccare qualsiasi operazione tentata dallo script

Questo concetto risolve una richiesta di supporto ricorrente: "il mio modulo d'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". Tutte queste situazioni fanno capo alla medesima architettura, ed è utile analizzarla per comprendere come essa definisca anche il perimetro di sicurezza contro documenti potenzialmente dannosi

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

PDFium tratta il JavaScript dei documenti rigorosamente con logica opt-in: se il membro m_pJsPlatform dell'ambiente di compilazione dei moduli è NULL, ogni script nel documento viene ignorato in modo silente. Nessun errore, nessuna riga di log, nessuna esecuzione parziale. I campi calcolati mantengono l'ultimo valore memorizzato, le chiamate a app.alert non hanno effetto e gli script di formattazione non vengono eseguiti. Acrobat, che include sempre il proprio motore JavaScript, esegue correttamente lo stesso file, motivo per cui la discrepanza appare come un bug nel tuo codice quando in realtà si tratta di un comportamento predefinito e intenzionale della libreria sottostante

Il Componente PDFium presentava in precedenza un ulteriore livello di vincolo. Fino alla versione 2.13.1 il binding collegava m_pJsPlatform solo all'interno del ramo di inizializzazione XFA, per cui i normali documenti AcroForm, che costituiscono la stragrande maggioranza dei PDF con script, non ricevevano alcuna piattaforma JavaScript anche in presenza della DLL abilitata per V8. La versione 2.13.2 ha rimosso questa associazione dall'inizializzazione XFA: InitializeFormFill compila la tabella IPDF_JsPlatform ogni volta che V8FeaturesAvailable restituisce True, indipendentemente dal fatto che il documento sia in formato XFA. Il risultato pratico è che gli script a livello di documento, le catene di calcolo dei campi e le istruzioni app.alert vengono ora eseguiti anche nei normali documenti AcroForm

Di quale DLL V8 ha bisogno JavaScript AcroForm in Delphi?

JavaScript AcroForm richiede un binario PDFium compilato con il motore V8, che il Componente PDFium carica come pdfium.v8.dll se l'opzione globale EnableV8Engine è impostata su True prima del primo caricamento della libreria. C'è un aspetto importante da considerare: il componente rileva automaticamente i documenti XFA eseguendo una scansione preliminare per individuare i marcatori /XFA e attiva l'opzione EnableV8Engine automaticamente, ma un normale documento AcroForm con JavaScript non contiene alcun marcatore XFA, per cui non avviene alcun rilevamento automatico. Se il tuo visualizzatore deve eseguire gli script dei moduli, imposta manualmente il flag una sola volta prima di caricare il primo documento; una volta caricata la libreria pdfium.dll standard, il processo non può cambiare motore

uses PDFium;

procedure TViewerForm.FormCreate(Sender: TObject);
begin
  // Must run before the singleton PDFium library first loads;
  // once plain pdfium.dll is in the process it cannot be swapped
  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;

Eventi host per avvisi, stampa, invio email e moduli

Ogni azione visibile eseguita da uno script viene reindirizzata al tuo codice tramite gli eventi di TPdf, e ciascuno di essi non esegue alcuna operazione di default se non assegnato. L'evento OnJavaScriptAlert riceve il messaggio, il titolo, il tipo di pulsante e l'icona da app.alert e consente di restituire il risultato del pulsante premuto; OnJavaScriptResponse gestisce le richieste di input di app.response, incluso il flag della password; OnJavaScriptBeep, OnJavaScriptPrint, OnJavaScriptMail e OnJavaScriptSubmitForm rilevano le richieste di segnale acustico, stampa, email e invio moduli con i relativi set di parametri. Un visualizzatore che non assegna nessuno di questi gestori esegue comunque il rendering e i calcoli correttamente; semplicemente, il documento non potrà aprire finestre di dialogo, stampare o trasmettere dati

procedure TViewerForm.PdfJavaScriptAlert(Sender: TObject;
  const Msg, Title: WString; ButtonType, Icon: Integer;
  var Result_: Integer; var Handled: Boolean);
begin
  // Route app.alert through the VCL so it is owned by our window
  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
  // Nothing is transmitted unless this handler chooses to send it
  LogAudit(Format('Document requested form submit to %s (%d bytes)',
    [Url, Length(Data)]));
end;

Questo comportamento predefinito è importante per la sicurezza. Gli eventi OnJavaScriptMail e OnJavaScriptSubmitForm sono di tipo notifica: il Componente PDFium non esegue alcuna azione di rete o MAPI autonomamente, per cui un documento dannoso che chiama this.submitForm o this.mailDoc contro un host non configurato non ottiene alcun risultato. L'host deve esplicitamente gestire l'evento e implementare il trasporto dei dati, il che significa che la decisione di trasmettere informazioni all'esterno spetta sempre al tuo codice, che dispone dell'URL e del payload

Come si impedisce a un PDF dannoso di avviare URL?

Le azioni URI rappresentano l'unico ambito in cui il comportamento predefinito originario era attivo anziché passivo, motivo per cui è stato introdotto un blocco dedicato. Prima della versione 2.13.1, la callback FFI_DoURIActionWithKeyboardModifier eseguiva tramite shell qualunque URI fornito da un campo XFA, in modo incondizionato, offrendo a un documento dannoso la possibilità di avviare URL arbitrari al clic. L'evento OnXfaUriAction risolve questa vulnerabilità: il componente consulta l'evento prima dell'esecuzione, e un gestore che imposta Handled su True sopprime completamente l'esecuzione predefinita tramite ShellExecuteW. Lasciare l'evento non assegnato mantiene il vecchio comportamento per compatibilità, per cui un visualizzatore attento alla sicurezza dovrebbe sempre configurarlo

procedure TViewerForm.PdfXfaUriAction(Sender: TObject;
  const Uri: WString; Modifiers: Integer; var Handled: Boolean);
begin
  // Veto anything that is not plain https; the default would
  // otherwise ShellExecuteW the URI as-is
  if not SameText(Copy(Uri, 1, 8), 'https://') then
  begin
    LogAudit('Blocked URI action: ' + Uri);
    Handled := True;
  end;
end;

Una allow-list sullo schema rappresenta il requisito minimo; un visualizzatore più restrittivo richiede la conferma all'utente o reindirizza tutto attraverso un componente browser in sandbox proprietario. La stessa filosofia di controllo si estende a tutto il set di eventi introdotti nella versione v2.13.1: OnXfaEmail, OnXfaHttpRequest, e OnXfaOpenFile espongono i parametri testuali dell'azione richiesta, mentre il componente non esegue alcuna operazione di rete o sui file. L'integrazione di questi controlli in una strategia più ampia per documenti non attendibili, incluso l'isolamento del rendering, è trattata nell'implementazione di un'anteprima PDF sicura in Delphi

Scrivere nel campo attivo con SetFocusedFormFieldText

Il metodo TPdf.SetFocusedFormFieldText, introdotto nella versione 2.13.2, è l'equivalente di scrittura di FocusedFormFieldText: seleziona il contenuto corrente del campo attivo tramite FORM_SelectAllText e lo sovrascrive con FORM_ReplaceSelection, esattamente come se l'utente avesse digitato la sostituzione. Poiché il testo viene inserito tramite il percorso di modifica interattivo, gli script di digitazione, formattazione e calcolo associati al campo si attivano come per l'input manuale, rendendola la primitiva corretta per la compilazione programmatica in un visualizzatore con JavaScript attivo. La navigazione dei campi e la gestione del focus sono trattate nella navigazione tra i campi del modulo con il Componente PDFium

if FPdf.FocusedFormFieldIndex >= 0 then
begin
  if not FPdf.SetFocusedFormFieldText('42.50') then
    ShowMessage('No focused field accepts text on this page');
  // AcroForm: value commits to /V when focus leaves the field.
  // XFA: the write stays in the in-memory edit buffer only and
  // will not survive a save
end;

L'asimmetria di persistenza indicata in questo commento rappresenta un limite strutturale. Per i campi di testo e le caselle combinate (combo) AcroForm, il valore modificato viene registrato nella voce /V del campo alla perdita del focus e persiste al salvataggio. Per i campi di testo XFA, la scrittura interessa solo il buffer di modifica in memoria, poiché PDFium non espone API pubbliche per riportare i valori dei widget nel pacchetto datasets XFA; di conseguenza, il salvataggio del documento non includerà la modifica. Se è richiesta la modifica persistente dei dati XFA, occorre modificare l'XML dei datasets direttamente invece del widget

Limiti da conoscere prima della distribuzione

Rimangono due limiti oggettivi. In primo luogo, le API JavaScript implementate da PDFium sono un sottoinsieme del modello a oggetti di Acrobat, incentrato sulla gestione dei moduli: accesso ai campi, eventi di calcolo e formattazione, app.alert e app.response, e richieste di stampa, invio email e moduli. I documenti che fanno affidamento su oggetti complessi espressamente esclusivi di Acrobat non funzioneranno, solitamente in modo silente, per cui si consiglia di verificare i moduli reali gestiti dagli utenti piuttosto che dare per scontata la compatibilità. In secondo luogo, abilitare il motore V8 richiede la distribuzione di pdfium.v8.dll, un file binario notevolmente più grande rispetto a quello standard; un visualizzatore che non necessiti di script o moduli XFA può utilizzare la DLL più piccola e omettere l'associazione di m_pJsPlatform, scelta che costituisce di per sé una valida misura di sicurezza

La procedura consigliata è lineare: imposta EnableV8Engine all'avvio, configura gli eventi di avviso e risposta in modo che le finestre di dialogo siano coerenti con il sistema host, assegna OnXfaUriAction e registra nei log le richieste di invio email e moduli, lasciando gli altri elementi alle impostazioni predefinite finché non sia richiesta una configurazione specifica. Il riferimento completo degli eventi e l'API di compilazione dei moduli sono documentati nella pagina del prodotto del Componente PDFium