Formulář, jehož celkové součty se v programu Acrobat přepočítávají, ale ve vašem vlastním prohlížeči v Delphi zůstávají nezměněné, je málokdy chybou vykreslování. Knihovna PDFium deaktivuje veškerý JavaScript v dokumentu, pokud hostitelská aplikace nepřipojí platformu JS. Komponenta PDFium Component tuto platformu připojuje automaticky od verze 2.13.2, takže JavaScript v AcroFormu, skripty na úrovni dokumentu a výpočty polí se spustí pokaždé, když je načtena knihovna DLL s podporou V8. Hostitelská aplikace si udržuje kontrolu prostřednictvím sady událostí, které mohou zobrazit, přesměrovat nebo zakázat cokoli, o co se skript pokusí
Tato jediná věta řeší dotazy podpory, se kterými jsme se setkali v různých podobách: „můj objednávkový formulář počítá mezisoučty řádků v Acrobatu, ale ne v mé aplikaci“, „událost app.alert se nikdy nespustí“, „pole s datem se samo neformátuje“. Všechny tyto problémy se vracejí k témuž principu a jeho pochopení stojí za deset minut času, protože stejná pravidla tvoří také vaši bezpečnostní hranici proti nepřátelským dokumentům
Proč výpočty formulářů v PDFium nefungují?
Knihovna PDFium přistupuje k JavaScriptu v dokumentu striktně jako k volitelnému (opt-in): pokud je člen m_pJsPlatform prostředí pro vyplňování formulářů roven NULL, každý skript v dokumentu je tiše přeskočen. Žádná chyba, žádný záznam v logu, žádné částečné spuštění. Vypočítaná pole si ponechají svou naposledy uloženou hodnotu, app.alert zmizí a formátovací skripty se nikdy nespustí. Program Acrobat, který je vždy dodáván s vlastním enginem JavaScriptu, stejný soubor bez problémů spustí — proto tento rozdíl vypadá jako chyba ve vašem kódu, přestože jde o záměrné výchozí nastavení v knihovně pod ním
Komponenta PDFium Component měla v tomto ohledu druhou, historickou vrstvu. Do verze 2.13.1 se vazba m_pJsPlatform připojovala pouze uvnitř inicializační větve XFA. Obyčejné dokumenty AcroForm, které představují naprostou většinu skriptovaných PDF, tak platformu JavaScriptu vůbec neobdržely, i když byla DLL knihovna s podporou V8 přítomna. Verze 2.13.2 toto připojení vyčlenila z rozhodování o XFA: metoda InitializeFormFill nyní sestaví tabulku IPDF_JsPlatform pokaždé, když vlastnost V8FeaturesAvailable vrátí hodnotu True, bez ohledu na to, zda jde o dokument XFA. Praktickým výsledkem je, že skripty na úrovni dokumentu, řetězce výpočtů polí a app.alert se nyní spouštějí i v běžných dokumentech AcroForm
Kterou knihovnu DLL V8 potřebuje AcroForm JavaScript v Delphi?
AcroForm JavaScript vyžaduje binární soubor PDFium zkompilovaný s enginem V8, který komponenta PDFium Component načítá jako pdfium.v8.dll, pokud je globální příznak EnableV8Engine nastaven na True ještě před prvním načtením knihovny. Je zde však jedno úskalí, které je třeba uvést na rovinu: komponenta automaticky detekuje dokumenty XFA předběžným skenováním značek /XFA a přepne EnableV8Engine za vás. Obyčejný dokument AcroForm s JavaScriptem však žádnou značku XFA nenese, takže k žádné automatické detekci nedojde. Pokud by váš prohlížeč měl spouštět formulářové skripty, nastavte tento příznak sami, a to jednou před načtením prvního dokumentu; po načtení standardní knihovny pdfium.dll již proces nemůže motory změnit
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;
Vlastnost V8FeaturesAvailable představuje spolehlivý test za běhu (runtime probe): informuje o tom, zda načtený binární soubor reálně obsahuje exporty závislé na V8, takže tato kontrola funguje bez ohledu na to, kterou knihovnu DLL instalační program nakonec nasadil. Stejný motor a stejný příznak pohánějí také dynamické vykreslování XFA; podrobnosti o tom, jak se prolíná detekce XFA a automatický výběr V8, popisuje článek o detekci XFA formulářů a extrakci XFA paketů pomocí PDFium
Události hostitele pro varování, tisk, e-mail a odesílání formulářů
Každá viditelná akce, kterou skript provede, se směruje zpět do vašeho kódu prostřednictvím událostí objektu TPdf, přičemž každá z nich se při nepřiřazení chová jako bezpečná prázdná operace (no-op). Událost OnJavaScriptAlert přijímá zprávu, titulek, typ tlačítka a ikonu z app.alert a umožňuje vrátit výsledek stisknutého tlačítka; OnJavaScriptResponse obsluhuje vstupní výzvy app.response včetně příznaku hesla; události OnJavaScriptBeep, OnJavaScriptPrint, OnJavaScriptMail a OnJavaScriptSubmitForm zpřístupňují požadavky na pípnutí, tisk, odeslání e-mailu a odeslání formuláře s jejich kompletními sadami parametrů. Prohlížeč, který žádnou z nich nepřiřadí, stále správně vykresluje i počítá; dokument pouze nemůže otevírat dialogy, tisknout ani nic odesílat
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;
Toto rozdělení výchozího chování je důležité pro modelování hrozeb. Události OnJavaScriptMail a OnJavaScriptSubmitForm fungují na principu pouhého oznámení: knihovna PDFium Component sama o sobě neprovádí žádné síťové akce ani volání MAPI, takže škodlivý dokument volající this.submitForm nebo this.mailDoc proti nepřipravenému hostiteli nedosáhne vůbec ničeho. Hostitel musí aktivně přiřadit obslužnou proceduru (handler) a implementovat samotný přenos sám. Rozhodnutí o odeslání dat z počítače je tak vždy plně na vás, provádí se ve vašem kódu a máte při něm k dispozici URL i přenášená data
Jak zabránit škodlivému PDF ve spouštění URL odkazů?
Akce URI jsou jediným místem, kde bylo historické výchozí chování aktivní namísto pasivního, a proto získaly vyhrazené právo veta. Před verzí 2.13.1 zpětné volání FFI_DoURIActionWithKeyboardModifier bezpodmínečně provádělo systémové spuštění (ShellExecute) jakékoli adresy URI, kterou pole XFA poskytlo, což škodlivému dokumentu dávalo možnost spouštět po kliknutí libovolné URL. Událost OnXfaUriAction tuto zranitelnost uzavírá: komponenta se před spuštěním dotáže na událost a handler nastavením Handled na hodnotu True výchozí volání ShellExecuteW zcela potlačí. Ponechání události nepřiřazené zachovává z důvodu kompatibility staré chování, takže prohlížeč dbající na bezpečnost by ji měl vždy přiřadit
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;
Minimem je seznam povolených schémat (allow-list); přísnější prohlížeč akci potvrdí s uživatelem nebo vše směruje přes vlastní izolovaný (sandboxed) prohlížečový komponent. Stejná filozofie veta se prolíná celou sadou událostí verze v2.13.1: události OnXfaEmail, OnXfaHttpRequest a OnXfaOpenFile zpřístupňují textové parametry vyžadované akce, zatímco samotná komponenta žádný síťový přenos ani práci se soubory neprovádí. Jak tyto ochrany zapadají do širší strategie pro práci s nedůvěryhodnými dokumenty (včetně izolace vykreslování) popisuje článek o vytvoření bezpečného náhledu PDF v Delphi
Zápis do aktivního pole pomocí SetFocusedFormFieldText
Metoda TPdf.SetFocusedFormFieldText přidaná ve verzi 2.13.2 je protějškem pro zápis k vlastnosti FocusedFormFieldText: vybere aktuální obsah aktivního pole přes FORM_SelectAllText a přepíše jej pomocí FORM_ReplaceSelection, přesně tak, jako by uživatel napsal náhradu na klávesnici. Vzhledem k tomu, že text vstupuje přes interaktivní editační cestu, veškeré skripty pro stisk kláves, formátování a výpočty připojené k poli se spustí stejně jako při ručním zadávání. To z této metody dělá správný základní prvek pro programové vyplňování v prohlížeči, který udržuje JavaScript aktivní. Procházení polí a správa zaměření (focusu) kolem něj jsou popsány v článku o navigaci ve formulářových polích 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;
Asymetrie ukládání popsaná v tomto komentáři představuje reálné omezení, nikoli jen formální upozornění. U textových a rozbalovacích polí AcroForm se upravená hodnota zapíše do položky /V pole při ztrátě zaměření a zůstane zachována i po uložení. U textových polí XFA se zápis uloží pouze do dočasné editační paměti, protože PDFium nenabízí žádné veřejné API pro promítnutí hodnot ovládacích prvků zpět do datových sad XFA; uložení dokumentu tuto změnu nepřenese. Pokud je vyžadována trvalá úprava dat XFA, upravujte přímo XML datových sad (datasets) namísto samotného vizuálního prvku
Omezní, která je dobré znát před nasazením
Zbývají dvě reálná omezení. Zaprvé, rozhraní JavaScript API, které PDFium implementuje, je funkční podmnožinou objektového modelu Acrobat zaměřenou na jádro skriptování formulářů: přístup k polím, události výpočtů a formátování, app.alert a app.response, tisk, e-mail a požadavky na odeslání. Dokumenty, které spoléhají na specifické objekty dostupné pouze v Acrobatu, budou degradovat (obvykle tiše), proto testujte reálné formuláře, se kterými vaši uživatelé pracují, namísto předpokladu plné shody. Zadruhé, povolení V8 znamená nutnost nasadit soubor pdfium.v8.dll, což je podstatně větší binární soubor než standardní sestavení. Prohlížeč, který skriptování ani XFA nikdy nepotřebuje, může legitimně ponechat menší DLL a ponechat m_pJsPlatform nepřipojené, což je samo o sobě platným bezpečnostním opatřením
Doporučený postup je příjemně jednoduchý: nastavte EnableV8Engine při spuštění, přiřaďte události pro upozornění a reakce, aby dialogy působily nativně, přiřaďte OnXfaUriAction a logujte události odesílání e-mailů i formulářů, a vše ostatní ponechte na výchozím nastavení (no-op), dokud nevyvstane jiný požadavek. Kompletní přehled událostí a API pro vyplňování formulářů naleznete na produktové stránce PDFium Component