Egy olyan űrlap, amelynek végösszegei újraszámolódnak Acrobatban, de befagyva állnak a saját Delphi megjelenítőjében, szinte soha nem renderelési hiba. A PDFium letilt minden dokumentum-JavaScriptet, hacsak a gazda (host) nem csatol hozzá egy JS platformot. A PDFium komponens ezt a platformot automatikusan beköti a 2.13.2-es verziótól kezdődően, így az AcroForm JavaScript, a dokumentumszintű scriptek és a mezőszámítások lefutnak, amikor a V8-támogatással rendelkező DLL betöltődik. A gazda továbbra is irányít egy eseménykészleten keresztül, amely megjelenítheti, átirányíthatja vagy megvétózhatja mindazt, amit a script megkísérel tenni.
Ez az egyetlen mondat megold egy támogatási kérdést, amelyet több formában is láttunk: 'a megrendelőlapom kiszámítja a részösszegeket Acrobatban, de az alkalmazásomban nem', 'az app.alert soha nem indul el', 'a dátummező nem formázza magát'. Mindezek ugyanarra a szerződésre vezethetők vissza, és ennek megértése megér tíz percet, mert ugyanez a szerződés a biztonsági határvonala is az ellenséges dokumentumokkal szemben.
Miért nem működnek a PDF űrlapszámítások a PDFiumban?
A PDFium a dokumentum-JavaScriptet szigorúan választható (opt-in) szolgáltatásként kezeli: ha az űrlapkitöltő környezet m_pJsPlatform tagja NULL, a dokumentumban lévő összes script csendben kimarad. Nincs hiba, nincs naplósor, nincs részleges végrehajtás. A számított mezők megtartják az utolsó tárolt értéküket, az app.alert eltűnik, a formázó scriptek pedig soha nem futnak le. Az Acrobat, amely mindig saját JavaScript motorral érkezik, boldogan futtatja ugyanazt a fájlt, ezért tűnik az eltérés olyannyira a kódjában lévő hibának, miközben ez valójában egy szándékos alapértelmezés a mögöttes könyvtárban.
A PDFium komponensnek volt egy második, történelmi rétege is ezzel kapcsolatban. A 2.13.1-es verzióig a kötés csak az XFA inicializálási ágán belül csatolta a m_pJsPlatform-ot, így a sima AcroForm dokumentumok — amelyek a scripted PDF-ek túlnyomó többségét alkotják — egyáltalán nem kaptak JavaScript platformot, még akkor sem, ha a V8-támogatású DLL jelen volt. A 2.13.2-es verzió kihúzta a csatolást az XFA-döntésből: az InitializeFormFill most már felépíti az IPDF_JsPlatform táblát, valahányszor a V8FeaturesAvailable True értékkel tér vissza, függetlenül attól, hogy a dokumentum XFA-e. A gyakorlati eredmény az, hogy a dokumentumszintű scriptek, a mezőszámítási láncok és az app.alert most már a normál AcroForm dokumentumokban is futnak.
Melyik V8 DLL-re van szüksége az AcroForm JavaScriptnek Delphiben?
Az AcroForm JavaScriptnek a V8 motorral fordított PDFium binárisra van szüksége, amelyet a PDFium komponens pdfium.v8.dll néven tölt be, ha a globális EnableV8Engine zászló True, még a könyvtár első betöltése előtt. Van itt egy csapda, amelyet érdemes nyíltan kimondani: a komponens automatikusan észleli az XFA dokumentumokat a /XFA jelölők előzetes beolvasásával, és átkapcsolja Ön helyett az EnableV8Engine-t, de a JavaScripttel ellátott egyszerű AcroForm dokumentum nem hordoz XFA jelölőt, így nem történik automatikus észlelés. If your viewer should run form scripts, set the flag yourself, once, before the first document loads; after a plain pdfium.dll is loaded the process cannot switch engines.
uses PDFium;
procedure TViewerForm.FormCreate(Sender: TObject);
begin
// Futnia kell az egyedüli (singleton) PDFium könyvtár első betöltése előtt;
// miután a sima pdfium.dll bekerült a folyamatba, már nem cserélhető le
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;
A V8FeaturesAvailable a valódi futásidejű szonda: jelzi, hogy a betöltött bináris valóban feloldja-e a V8-függő exportálásokat, így az ellenőrzés működik, függetlenül attól, hogy a telepítő végül melyik DLL-t helyezte el. Ugyanez a motor és ugyanez a zászló vezérli a dinamikus XFA renderelést is; az XFA észlelés és a V8 automatikus kiválasztásának kölcsönhatásáról a XFA űrlapok észlelése és XFA csomagok kinyerése a PDFiummal című cikkben olvashat bővebben.
Gazdaesemények riasztásokhoz, nyomtatáshoz, levelezéshez és űrlapküldéshez
Minden látható dolog, amit egy script végez, a TPdf eseményeken keresztül visszajut a kódjába, és mindegyikük alapértelmezés szerint biztonságos üres műveletre (no-op) áll be, ha nincs hozzárendelve semmi. Az OnJavaScriptAlert megkapja az üzenetet, a címet, a gomb típusát és az ikont az app.alert-től, és lehetővé teszi a megnyomott gomb eredményének visszaadását; az OnJavaScriptResponse kiszolgálja az app.response beviteli felkéréseket, beleértve a jelszó zászlót is; az OnJavaScriptBeep, OnJavaScriptPrint, OnJavaScriptMail és OnJavaScriptSubmitForm felszínre hozzák a csipogás, nyomtatás, levelezés és beküldés kéréseit a teljes paraméterkészletükkel. Az a megjelenítő, amelyik ezek egyikét sem rendeli hozzá, továbbra is megfelelően renderel és számol; a dokumentum egyszerűen nem tud párbeszédpaneleket megnyitni, nyomtatni vagy bármit elküldeni.
procedure TViewerForm.PdfJavaScriptAlert(Sender: TObject;
const Msg, Title: WString; ButtonType, Icon: Integer;
var Result_: Integer; var Handled: Boolean);
begin
// Irányítsuk át az app.alert-et a VCL-en keresztül, hogy az ablakunk tulajdonában legyen
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
// Semmi sem kerül továbbításra, hacsak ez a kezelő nem dönt úgy, hogy elküldi
LogAudit(Format('Document requested form submit to %s (%d bytes)',
[Url, Length(Data)]));
end;
Az alapértelmezett viselkedés felosztása fontos a fenyegetések modellezése (threat modeling) szempontjából. Az OnJavaScriptMail és az OnJavaScriptSubmitForm értesítés-stílusúak: a PDFium komponens önmagában nem végez hálózati vagy MAPI műveletet, így egy rosszindulatú dokumentum, amely meghívja a this.submitForm-ot vagy a this.mailDoc-ot egy felkészületlen gazdával szemben, pontosan a semmit éri el. A gazdának feliratkozással (opt-in) kell hozzájárulnia egy kezelő hozzárendelésével és a szállítás saját maga általi megvalósításával, ami azt jelenti, hogy az adatok gépből való kijuttatásáról szóló döntés mindig az Öné, a kódjában születik meg, az URL-lel és a hasznos teherrel (payload) a kezében.
Hogyan akadályozhatjuk meg, hogy egy rosszindulatú PDF URL-eket indítson el?
Az URI műveletek jelentették az egyetlen olyan helyet, ahol a történelmi alapértelmezés aktív volt a passzív helyett, és ezért kaptak dedikált vétójogot. A 2.13.1-es verzió előtt a FFI_DoURIActionWithKeyboardModifier visszahívás feltétel nélkül végrehajtotta (shell-executed) az XFA mező által megadott bármely URI-t, ami egy rosszindulatú dokumentumnak lehetőséget adott tetszőleges URL-ek indítására kattintáskor. Az OnXfaUriAction bezárja ezt a rést: a komponens a végrehajtás előtt konzultál az eseménnyel, és a Handled-et True-ra állító kezelő teljesen elnyomja a ShellExecuteW alapértelmezett viselkedését. Az esemény hozzárendelés nélkül hagyása a kompatibilitás érdekében megőrzi a régi viselkedést, így a biztonságtudatos megjelenítőnek ezt mindig hozzá kell rendelnie.
procedure TViewerForm.PdfXfaUriAction(Sender: TObject;
const Uri: WString; Modifiers: Integer; var Handled: Boolean);
begin
// Vétózzunk meg mindent, ami nem tiszta https; az alapértelmezett viselkedés
// ellenkező esetben a ShellExecuteW a megadott URI-t úgy futtatná le, ahogy van
if not SameText(Copy(Uri, 1, 8), 'https://') then
begin
LogAudit('Blocked URI action: ' + Uri);
Handled := True;
end;
end;
A séma engedélyezési listája a minimum; egy szigorúbb megjelenítő egyeztet a felhasználóval, vagy mindent a saját homokozóba zárt (sandboxed) böngészőkomponensén keresztül irányít át. Ugyanez a vétófilozófia terjed ki a v2.13.1-es eseménykészletre is: az OnXfaEmail, az OnXfaHttpRequest és az OnXfaOpenFile mindegyike felfedi a kért művelet szöveges paramétereit, miközben maga a komponens nem végez hálózati vagy fájltranszportot. Az, hogy ezek a védelmi vonalak hogyan illeszkednek egy tágabb, nem megbízható dokumentumokra vonatkozó stratégiába, beleértve a renderelési izolációt is, a biztonságos PDF-előnézet felépítése Delphiben című cikk témája.
Írás a fókuszált mezőbe a SetFocusedFormFieldText segítségével
A 2.13.2-es verzióban hozzáadott TPdf.SetFocusedFormFieldText az írási oldali párja a FocusedFormFieldText-nek: a FORM_SelectAllText segítségével kijelöli a fókuszált mező aktuális tartalmát, és felülírja azt a FORM_ReplaceSelection segítségével, pontosan úgy, mintha a felhasználó gépelte volna be a helyettesítést. Mivel a szöveg az interaktív szerkesztési útvonalon keresztül lép be, a mezőhöz csatolt billentyűleütési, formázási és számítási scriptek ugyanúgy lefutnak, mint a kézi bevitel esetén, ami a megfelelő alapműveletté (primitive) teszi a programozott kitöltéshez a JavaScriptet élőben tartó megjelenítőkben. A mezők bejárása és a fókusz kezelése e körül a űrlapmezők navigációja a PDFium komponenssel című cikkben olvasható.
if FPdf.FocusedFormFieldIndex >= 0 then
begin
if not FPdf.SetFocusedFormFieldText('42.50') then
ShowMessage('No focused field accepts text on this page');
// AcroForm: az érték elköteleződik a /V-hez, amikor a fókusz elhagyja a mezőt.
// XFA: az írás csak a memórián belüli szerkesztési pufferben marad, és
// nem éli túl a mentést
end;
Az ebben a megjegyzésben szereplő perzisztencia-aszimmetria valódi határ, nem csupán formai figyelmeztetés. Az AcroForm szöveges és kombinált mezőinél a szerkesztett érték a fókusz elvesztésekor elköteleződik a mező /V bejegyzéséhez, és a mentés során is megmarad. Az XFA szöveges mezőknél az írás csak a memórián belüli szerkesztési pufferbe kerül, mivel a PDFium nem tesz elérhetővé nyilvános API-t a widget-értékek XFA adatkészletek (datasets) csomagjával való egyeztetésére; a dokumentum mentése nem fogja hordozni a változást. Ha a tartós XFA adat-szerkesztés a követelmény, szerkessze magát az adatkészletek XML-jét a widget helyett.
Határok, amelyeket érdemes tudni a szállítás előtt
Két tiszta határvonal marad. Először is, a PDFium által implementált JavaScript API az Acrobat objektummodell egy működő részhalmaza, amely az űrlap-scripting magjára összpontosít: mezőelérés, számítási és formázási események, app.alert és app.response, nyomtatási, levelezési és beküldési kérések. Az egzotikus, csak Acrobat-specifikus objektumokra támaszkodó dokumentumok minősége romlani fog (degrade), általában csendben, ezért tesztelje azokat a tényleges űrlapokat, amelyeket a felhasználói kezelnek, ahelyett hogy paritást feltételezne. Másodszor, a V8 engedélyezése a pdfium.v8.dll szállítását jelenti, ami lényegesen nagyobb bináris fájl, mint a sima verzió; egy olyan megjelenítő, amelynek soha sincs szüksége scriptingre vagy XFA-ra, jogosan megtarthatja a kisebb DLL-t, és csatolás nélkül hagyhatja az m_pJsPlatform-ot, ami önmagában is érvényes biztonsági hozzáállás.
Az mindebből kirajzolódó minta kellemesen unalmas: állítsa be az EnableV8Engine-t indításkor, rendelje hozzá a riasztási (alert) és válaszadási (response) eseményeket, hogy a párbeszédpanelek natívnak tűnjenek, rendelje hozzá az OnXfaUriAction-t, naplózza a levelezési és beküldési eseményeket, és hagyjon minden mást a no-op alapértelmezésen, hacsak egy követelmény másként nem rendelkezik. A teljes esemény-referencia és az űrlapkitöltő API-felület dokumentálva van a PDFium komponens termékoldalán.