Tehnični članek

Zagon AcroForm JavaScripta v Delphiju s komponento PDFium Component

Obrazec, katerega skupne vsote se v Acrobatu ponovno izračunajo, v vašem lastnem pregledovalniku Delphi pa ostanejo zamrznjene, skoraj nikoli ni napaka pri upodabljanju. Knjižnica PDFium onemogoči ves JavaScript dokumenta, razen če gostitelj priključi platformo JS, komponenta PDFium Component pa to platformo samodejno poveže od različice 2.13.2 naprej, tako da se AcroForm JavaScript, skripti na ravni dokumenta in izračuni polj izvajajo vedno, ko je naložena knjižnica DLL s podporo za V8. Gostitelj ohranja nadzor prek nabora dogodkov, ki lahko prikažejo, preusmerijo ali preprečijo (veto) vse, kar skript poskuša storiti

Ta en sam stavek rešuje vprašanje za podporo, ki smo ga videli v več oblikah: "moj naročilni obrazec izračunava vsote vrstic v Acrobatu, v moji aplikaciji pa ne", "app.alert se nikoli ne sproži", "polje z datumom se ne oblikuje samodejno". Vse te težave izvirajo iz iste pogodbe in razumevanje le-te je vredno desetih minut, saj je ista pogodba tudi vaša varnostna meja pred sovražnimi dokumenti

Zakaj izračuni obrazcev PDF ne delujejo v PDFiumu?

PDFium obravnava JavaScript dokumenta kot strogo neobvezen (opt-in): če je član m_pJsPlatform okolja za izpolnjevanje obrazcev NULL, se vsak skript v dokumentu tiho preskoči. Ni napake, ni vrstice v dnevniku, ni delnega izvajanja. Izračunana polja ohranijo svojo zadnjo shranjeno vrednost, app.alert izgine, skripti za oblikovanje se nikoli ne zaženejo. Acrobat, ki vedno vsebuje lasten pogon JavaScript, veselo poganja isto datoteko, zato je ta razlika videti kot napaka v vaši kodi, čeprav gre dejansko za namerno privzeto nastavitev v spodnji knjižnici

Komponenta PDFium Component je imela pri tem še eno zgodovinsko plast. Do različice 2.13.1 je povezava priključila m_pJsPlatform le znotraj inicializacijske veje XFA, tako da navadni dokumenti AcroForm, ki predstavljajo večino skriptnih PDF-jev, nikoli niso prejeli platforme JavaScript, tudi ko je bil DLL s podporo za V8 prisoten. Različica 2.13.2 je priključitev izvlekla iz odločitve XFA: metoda InitializeFormFill zdaj zgradi tabelo IPDF_JsPlatform vedno, ko funkcija V8FeaturesAvailable vrne True, ne glede na to, ali gre za dokument XFA. Praktični rezultat je, da se skripti na ravni dokumenta, verige izračunov polj in app.alert zdaj izvajajo tudi v običajnih dokumentih AcroForm

Kateri V8 DLL potrebuje AcroForm JavaScript v Delphiju?

AcroForm JavaScript potrebuje PDFium binarno datoteko, prevedeno s pogonom V8, ki jo PDFium Component naloži kot pdfium.v8.dll, ko je pred prvim nalaganjem knjižnice globalna zastavica EnableV8Engine nastavljena na True. Tukaj je ena past, ki jo je treba odkrito povedati: komponenta samodejno zazna dokumente XFA s predhodnim iskanjem oznak /XFA in namesto vas vklopi EnableV8Engine, toda navaden dokument AcroForm z JavaScriptom ne nosi oznake XFA, zato se samodejno zaznavanje ne zgodi. Če mora vaš pregledovalnik poganjati skripte obrazcev, sami nastavite zastavico, in sicer enkrat, preden se naloži prvi dokument; ko se naloži navaden pdfium.dll, proces ne more več zamenjati pogonov

uses PDFium;

procedure TViewerForm.FormCreate(Sender: TObject);
begin
  // Zažene se pred prvim nalaganjem knjižnice PDFium;
  // ko je navaden pdfium.dll enkrat v procesu, ga ni mogoče zamenjati
  EnableV8Engine := True;
end;

procedure TViewerForm.OpenDocument(const FileName: string);
begin
  FPdf.LoadDocument(FileName);
  if not V8FeaturesAvailable then
    StatusBar.SimpleText :=
      'JavaScript onemogočen: pdfium.v8.dll ni nameščen';
end;

Lastnost V8FeaturesAvailable je zanesljiva preiskava med izvajanjem: poroča o tem, ali naložena binarna datoteka dejansko razrešuje izvoze, odvisne od V8, tako da preverjanje deluje ne glede na to, kateri DLL je namestitveni program na koncu namestil. Isti pogon in ista zastavica poganjata tudi dinamično upodabljanje XFA; ozadje o tem, kako sodelujeta zaznavanje XFA in samodejna izbira V8, je opisano v zaznavanju obrazcev XFA in ekstrakciji paketov XFA s komponento PDFium

Gostiteljski dogodki za opozorila, tiskanje, pošto in oddajo obrazcev

Vsaka vidna stvar, ki jo naredi skript, se prek dogodkov TPdf usmeri nazaj v vašo kodo in vsak od njih privzeto ne naredi ničesar (safe no-op), ko ni dodeljen. Dogodek OnJavaScriptAlert prejme sporočilo, naslov, vrsto gumba in ikono iz app.alert ter vam omogoča, da vrnete rezultat pritisnjenega gumba; OnJavaScriptResponse oskrbuje pozive za vnos app.response, vključno z zastavico za geslo; OnJavaScriptBeep, OnJavaScriptPrint, OnJavaScriptMail in OnJavaScriptSubmitForm pa na površje prinašajo zahteve za pisk, tiskanje, pošto in oddajo obrazca s polnimi nabori parametrov. Pregledovalnik, ki ne dodeli nobenega od teh dogodkov, še vedno pravilno upodablja in izračunava; dokument preprosto ne more odpirati pogovornih oken, tiskati ali pošiljati česar koli

procedure TViewerForm.PdfJavaScriptAlert(Sender: TObject;
  const Msg, Title: WString; ButtonType, Icon: Integer;
  var Result_: Integer; var Handled: Boolean);
begin
  // Usmeritev app.alert prek VCL, da je lastnik naše okno
  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
  // Nič se ne prenese, razen če rokovnik izbere pošiljanje
  LogAudit(Format('Dokument je zahteval oddajo obrazca na %s (%d bajtov)',
    [Url, Length(Data)]));
end;

Razdelitev privzetega obnašanja je pomembna za modeliranje groženj. Dogodka OnJavaScriptMail in OnJavaScriptSubmitForm sta le obvestilnega tipa: komponenta PDFium Component sama po sebi ne izvaja omrežnih ali MAPI dejanj, zato zlonamerni dokument, ki pokliče this.submitForm ali this.mailDoc proti nepripravljenemu gostitelju, ne doseže popolnoma ničesar. Gostitelj se mora vključiti z dodelitvijo rokovnika in samostojno implementacijo prenosa, kar pomeni, da je odločitev o prenosu podatkov z računalnika vedno vaša, sprejeta v vaši kodi, z naslovom URL in vsebino v rokah

Kako zlonamernemu PDF-ju preprečiti odpiranje naslovov URL?

Dejanja URI so edino mesto, kjer je bilo zgodovinsko privzeto obnašanje aktivno in ne pasivno, zato so dobila namenski veto. Pred različico 2.13.1 je povratni klic FFI_DoURIActionWithKeyboardModifier brezpogojno izvedel (shell-executed) kateri koli URI, ki ga je posredovalo polje XFA, kar je zlonamernemu dokumentu ob kliku omogočilo zagon poljubnih naslovov URL. Dogodek OnXfaUriAction to zapira: komponenta se pred izvedbo posvetuje z dogodkom, rokovnik, ki nastavi Handled na True, pa popolnoma zatre privzeto izvedbo ShellExecuteW. Če dogodek pustite nedodeljen, se zaradi združljivosti ohrani staro obnašanje, zato bi ga moral varnostno ozaveščen pregledovalnik vedno dodeliti

procedure TViewerForm.PdfXfaUriAction(Sender: TObject;
  const Uri: WString; Modifiers: Integer; var Handled: Boolean);
begin
  // Preprečitev vsega, kar ni čist https; privzeto bi
  // sicer izvedlo ShellExecuteW na URI-ju, kakršen je
  if not SameText(Copy(Uri, 1, 8), 'https://') then
  begin
    LogAudit('Blokirano dejanje URI: ' + Uri);
    Handled := True;
  end;
end;

Seznam dovoljenih shem (allow-list) je minimum; strožji pregledovalnik potrdi dejanje z uporabnikom ali pa vse usmeri prek svoje lastne brskalniške komponente v peskovniku (sandboxed). Enaka filozofija veta se razteza čez nabor dogodkov različice v2.13.1: OnXfaEmail, OnXfaHttpRequest in OnXfaOpenFile izpostavijo besedilne parametre zahtevanega dejanja, medtem ko komponenta sama ne izvaja omrežnega ali datotečnega prenosa. Kako se ta varovala uvrščajo v širšo strategijo za nezaupljive dokumente, vključno z izolacijo upodabljanja, je tema članka o izdelavi varnega predogleda PDF v Delphiju

Zapisovanje v fokusirano polje z metodo SetFocusedFormFieldText

Metoda TPdf.SetFocusedFormFieldText, dodana v različici 2.13.2, je pisalni spremljevalec lastnosti FocusedFormFieldText: izbere trenutno vsebino fokusiranega polja prek FORM_SelectAllText in jo prepiše prek FORM_ReplaceSelection, natanko tako, kot če bi uporabnik vtipkal zamenjavo. Ker besedilo vstopi prek interaktivne poti za urejanje, se vsi skripti za pritiske tipk, oblikovanje in izračunavanje, pripeti na polje, sprožijo na enak način kot pri ročnem vnosu, zaradi česar je to pravi primitiv za programsko izpolnjevanje v pregledovalniku, ki ohranja JavaScript aktiven. Prehajanje polj in upravljanje fokusa okoli njega sta opisana v navigaciji med polji obrazcev s komponento PDFium Component

if FPdf.FocusedFormFieldIndex >= 0 then
begin
  if not FPdf.SetFocusedFormFieldText('42.50') then
    ShowMessage('Nobeno fokusirano polje na tej strani ne sprejema besedila');
  // AcroForm: vrednost se zapiše v /V, ko fokus zapusti polje.
  // XFA: zapis ostane le v urejevalnem medpomnilniku v pomnilniku in
  // ne bo preživel shranjevanja
end;

Asimetrija vztrajnosti v tem komentarju je dejanska meja in ne le opozorilo zaradi forme. Pri besedilnih in kombiniranih poljih AcroForm se urejena vrednost ob izgubi fokusa zapiše v vnos /V polja in ostane shranjena. Pri besedilnih poljih XFA pa zapis pristane le v urejevalnem medpomnilniku v pomnilniku, saj PDFium ne ponuja javnega API-ja za uskladitev vrednosti gradnikov nazaj v paket podatkov XFA datasets; shranjevanje dokumenta ne bo preneslo te spremembe. Če je zahteva trajno urejanje podatkov XFA, uredite sam XML datasets in ne gradnika

Omejitve, ki jih je dobro poznati pred izdajo izdelka

Ostajata dve stvarni omejitvi. Prvič, API za JavaScript, ki ga PDFium implementira, je delujoča podmnožica Acrobatovega objektnega modela, osredotočena na jedro skriptiranja obrazcev: dostop do polj, dogodki izračuna in oblikovanja, app.alert in app.response ter zahteve za tiskanje, pošto in oddajo. Dokumenti, ki se zanašajo na eksotične objekte, specifične le za Acrobat, bodo degradirali, običajno tiho, zato preizkusite dejanske obrazce, s katerimi delajo vaši uporabniki, namesto da bi predvidevali popolno usklajenost. Drugič, omogočanje V8 pomeni pošiljanje datoteke pdfium.v8.dll, ki je bistveno večja binarna datoteka kot navadna različica; pregledovalnik, ki nikoli ne potrebuje skriptiranja ali XFA, lahko legitimno obdrži manjšo knjižnico DLL in pusti m_pJsPlatform nepovezan, kar je že samo po sebi veljavno varnostno stališče

Vzorec, ki izhaja iz vsega tega, je prijetno dolgočasen: nastavite EnableV8Engine ob zagonu, dodelite dogodke za opozorila in odgovore, da bodo pogovorna okna videti domače, dodelite OnXfaUriAction in beležite dogodke pošte ter oddaje v revizijski dnevnik (audit log), vse ostalo pa pustite na privzetih vrednostih brez dejanj (no-op), dokler zahteva ne določa drugače. Celotna referenca dogodkov in API za izpolnjevanje obrazcev sta dokumentirana na strani izdelka PDFium Component