Odborný článok

Spúšťanie AcroForm JavaScriptu v Delphi pomocou PDFium Component

Formulár, ktorého celkové sumy sa v programe Acrobat prepočítavajú, no vo vašom vlastnom prehliadači v Delphi zostávajú nehybné, nie je takmer nikdy chybou vykresľovania. PDFium predvolene vypína všetok JavaScript v dokumente, pokiaľ hostiteľ nepripojí platformu JS. PDFium Component túto platformu pripája automaticky od verzie 2.13.2, takže JavaScript v AcroFormulároch, skripty na úrovni dokumentu a výpočty polí sa vykonávajú vždy, keď je načítaná knižnica DLL s povoleným jadrom V8. Hostiteľ si zachováva plnú kontrolu prostredníctvom sady udalostí, ktoré môžu zobraziť, presmerovať alebo vetovať všetko, o čo sa skript pokúsi

Táto jediná veta rieši otázku podpory, s ktorou sme sa stretli vo viacerých podobách: „môj objednávkový formulár počíta celkové sumy v Acrobate, ale nie v mojej aplikácii“, „app.alert sa nikdy nespustí“, „pole dátumu sa neformátuje“. Všetky tieto problémy pramenia z rovnakej zmluvy a jej pochopenie stojí za tých desať minút, pretože tá istá zmluva predstavuje aj vašu bezpečnostnú hranicu voči nebezpečným dokumentom

Prečo nefungujú výpočty vo formulároch PDF v knižnici PDFium?

PDFium pristupuje k JavaScriptu v dokumente striktne spôsobom opt-in: ak je člen m_pJsPlatform prostredia pre vypĺňanie formulárov nastavený na NULL, každý skript v dokumente sa potichu preskočí. Žiadna chyba, žiadny riadok v denníku, žiadne čiastočné vykonanie. Vypočítané polia si ponechajú svoju naposledy uloženú hodnotu, app.alert zmizne a formátovacie skripty sa nespustia. Acrobat, ktorý sa dodáva s vlastným JavaScript enginom, rovnaký súbor bez problémov spustí, čo je dôvod, prečo táto nezhoda vyzerá ako chyba vo vašom kóde, hoci v skutočnosti ide o zámerné predvolené nastavenie knižnice

PDFium Component mal v minulosti ešte druhú vrstvu tohto problému. Do verzie 2.13.1 prepojenie pripájalo m_pJsPlatform iba vo vnútri vetvy inicializácie XFA, takže obyčajné dokumenty AcroForm (ktoré predstavujú drvivú väčšinu skriptovaných PDF) nikdy nezískali platformu JavaScript, a to ani vtedy, keď bola prítomná DLL s podporou V8. Verzia 2.13.2 vytiahla toto pripojenie z rozhodovania o XFA: InitializeFormFill teraz zostavuje tabuľku IPDF_JsPlatform vždy, keď funkcia V8FeaturesAvailable vráti True, bez ohľadu na to, či ide o dokument XFA. Praktickým výsledkom je, že skripty na úrovni dokumentu, reťazce výpočtov polí a app.alert sa teraz vykonávajú aj v bežných dokumentoch AcroForm

Ktorú V8 DLL potrebuje AcroForm JavaScript v Delphi?

JavaScript v AcroFormulároch vyžaduje binárny súbor PDFium kompilovaný s enginom V8, ktorý PDFium Component načítava ako pdfium.v8.dll, ak je pred prvým načítaním knižnice globálny prepínač EnableV8Engine nastavený na True. Je tu jedna pasca, ktorú je potrebné uviesť na rovinu: komponent automaticky deteguje dokumenty XFA predbežným skenovaním značiek /XFA a prepne EnableV8Engine za vás, ale obyčajný dokument AcroForm s JavaScriptom žiadnu značku XFA nenesie, takže k automatickej detekcii nedôjde. Ak má váš prehliadač spúšťať formulárové skripty, nastavte prepínač sami, raz, ešte pred načítaním prvého dokumentu; po načítaní obyčajnej knižnice pdfium.dll už proces nemôže zmeniť engine

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;

Vlastnosť V8FeaturesAvailable je overením za behu programu: hlási, či načítaná knižnica skutočne obsahuje exporty závislé od V8, takže táto kontrola funguje bez ohľadu na to, ktorú DLL inštalačný program nasadil. Rovnaké jadro a rovnaký príznak poháňajú aj dynamické vykresľovanie XFA; podrobnosti o tom, ako spolupracuje detekcia XFA a automatický výber V8, sú popísané v detekcii XFA formulárov a extrakcii XFA balíkov s PDFium

Udalosti hostiteľa pre upozornenia, tlač, poštu a odoslanie formulára

Každá viditeľná akcia, ktorú skript vykoná, smeruje späť do vášho kódu prostredníctvom udalostí triedy TPdf, pričom každá z nich je predvolene nastavená ako bezpečná prázdna operácia, ak nie je priradená. Udalosť OnJavaScriptAlert pripomína správu, názov, typ tlačidla a ikonu z app.alert a umožňuje vám vrátiť stlačené tlačidlo; OnJavaScriptResponse obsluhuje vstupy app.response vrátane prepínača pre heslo; OnJavaScriptBeep, OnJavaScriptPrint, OnJavaScriptMail a OnJavaScriptSubmitForm zachytávajú žiadosti o pípnutie, tlač, odoslanie pošty a odoslanie formulára s ich kompletnými sadami parametrov. Prehliadač, ktorý nepriradí žiadnu z týchto udalostí, stále správne vykresľuje a počíta; dokument len nemôže otvárať dialógy, tlačiť ani nič odosielať

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;

Rozdelenie predvoleného správania je dôležité pre modelovanie hrozieb. Udalosti OnJavaScriptMail a OnJavaScriptSubmitForm majú charakter oznámení: PDFium Component nevykonáva sám o sebe žiadnu sieťovú akciu ani volanie MAPI, takže škodlivý dokument, ktorý volá this.submitForm alebo this.mailDoc voči nepripravenému hostiteľovi, nedosiahne vôbec nič. Hostiteľ sa musí explicitne prihlásiť priradením obslužnej procedúry a implementáciou samotného prenosu, čo znamená, že rozhodnutie presunúť údaje mimo počítača je vždy na vás, vo vašom kóde, s URL adresou a obsahom v rukách

Ako zabrániť škodlivému PDF spúšťať URL adresy?

Akcie URI sú jediným miestom, kde bolo historické predvolené nastavenie aktívne a nie pasívne, a preto získali dedikované právo veta. Pred verziou 2.13.1 callback FFI_DoURIActionWithKeyboardModifier bezpodmienečne spúšťal (cez ShellExecute) akékoľvek URI dodané poľom XFA, čo poskytovalo škodlivému dokumentu možnosť spúšťať po kliknutí ľubovoľné URL adresy. Udalosť OnXfaUriAction to uzatvára: komponent sa pred vykonaním radí s touto udalosťou a obslužný program, ktorý nastaví Handled na True, úplne potlačí predvolenú akciu ShellExecuteW. Ponechanie udalosti nepriradenej zachováva staré správanie kvôli spätnej kompatibilite, preto by ju prehliadač dbajúci na bezpečnosť mal vždy priradiť

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;

Povolený zoznam (allow-list) protokolov je minimom; prísnejší prehliadač si vyžiada potvrdenie od používateľa alebo všetko smeruje cez vlastný prehliadač v pieskovisku (sandbox). Rovnaká filozofia práva veta sa vzťahuje na celú sadu udalostí verzie v2.13.1: OnXfaEmail, OnXfaHttpRequest a OnXfaOpenFile sprístupňujú textové parametre požadovanej akcie, zatiaľ čo samotný komponent nevykonáva žiadnu sieťovú ani diskovú operáciu. Ako tieto ochranné prvky zapadajú do širšej stratégie narábania s nedôveryhodnými dokumentmi, vrátane izolácie vykresľovania, je témou článku o zostavení bezpečného náhľadu PDF v Delphi

Zápis do aktívneho poľa pomocou SetFocusedFormFieldText

Metóda TPdf.SetFocusedFormFieldText pridaná vo verzii 2.13.2 je zápisovým ekvivalentom k FocusedFormFieldText: vyberie aktuálny obsah aktívneho poľa cez FORM_SelectAllText a prepíše ho prostredníctvom FORM_ReplaceSelection, presne tak, ako keby používateľ zadal náhradu z klávesnice. Pretože text vstupuje cez interaktívnu editačnú cestu, akékoľvek skripty pre stlačenie klávesu, formátovanie a výpočty pripojené k tomuto poľu sa spúšťajú rovnako ako pri ručnom vstupe, čo z tejto metódy robí správny nástroj pre programové vypĺňanie v prehliadači, ktorý má spustený JavaScript. Prechádzanie polí a správa zamerania sú popísané v navigácii vo formulárových poliach s 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: 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;

Asymetria pretrvávania spomenutá v komentári predstavuje reálnu hranicu a nie len formálne varovanie. Pri textových a combo poliach v AcroFormulároch sa upravená hodnota zapíše do kľúča /V poľa pri strate zamerania a pretrváva aj po uložení. Pri textových poliach XFA zápis skončí iba v pamäťovom bufferi úprav, pretože PDFium neposkytuje žiadne verejné API na zosúladenie hodnôt widgetov späť do dátových balíkov XFA; uloženie dokumentu túto zmenu neprenesie. Ak je vašou požiadavkou trvalá úprava údajov XFA, upravte priamo XML datasets a nie vizuálny widget

Limity, ktoré sa oplatí poznať pred nasadením

Zostávajú dve skutočné obmedzenia. Po prvé, JavaScript API implementované v PDFium predstavuje funkčnú podmnožinu objektového modelu Acrobatu, zameranú na jadro skriptovania formulárov: prístup k poliam, udalosti výpočtov a formátovania, app.alert a app.response, požiadavky na tlač, poštu a odoslanie. Dokumenty, ktoré sa spoliehajú na neštandardné objekty dostupné iba v Acrobate, budú degradovať, zvyčajne potichu, preto otestujte reálne formuláre, s ktorými vaši používatelia pracujú. Po druhé, povolenie V8 znamená distribúciu súboru pdfium.v8.dll, čo je výrazne väčšia knižnica než bežná kompilácia; prehliadač, ktorý skriptovanie ani XFA nepotrebuje, môže úplne legitímne fungovať s menšou DLL a nechať m_pJsPlatform nepripojený, čo je samo o sebe platný bezpečnostný postoj

Postup vyplývajúci z tohto všetkého je príjemne jednoduchý: na začiatku nastavte EnableV8Engine, priraďte udalosti pre upozornenia a odozvu, aby dialógy vyzerali prirodzene, priraďte OnXfaUriAction a zaznamenávajte udalosti odoslania pošty a formulára, pričom všetko ostatné ponechajte v predvolenom stave bez reakcie, kým sa neobjaví iná požiadavka. Kompletná referencia udalostí a rozhranie pre vypĺňanie formulárov sú zdokumentované na produktovej stránke PDFium Component