טופס שסיכומיו מחושבים מחדש ב-Acrobat אך נשארים קפואים במציג ה-Delphi שלכם כמעט אף פעם אינו באג רינדור. ספריית PDFium מבטלת כל JavaScript במסמך אלא אם כן המארח מחבר פלטפורמת JS, ורכיב PDFium מחבר פלטפורמה זו באופן אוטומטי מגרסה 2.13.2 ואילך, כך ש-AcroForm JavaScript, סקריפטים ברמת המסמך וחישובי שדות מופעלים בכל פעם שקובץ ה-DLL מופעל-V8 נטען. המארח נשאר אחראי באמצעות סט של אירועים שיכולים להציג, לנתב מחדש או להטיל וטו על כל מה שהסקריפט מנסה לעשות
הבנה של חוזה זה שווה עשר דקות מכיוון שאותו חוזה הוא גם גבול האבטחה שלכם מפני מסמכים עוינים
מדוע חישובי טופסי PDF אינם עובדים ב-PDFium?
ספריית PDFium מתייחסת ל-JavaScript במסמכים כהרשאה מפורשת (opt-in): אם החבר m_pJsPlatform של סביבת מילוי הטפסים הוא NULL, כל סקריפט במסמך מדולג בשקט. ללא שגיאה, ללא שורת לוג, ללא הרצה חלקית. שדות מחושבים שומרים על הערך האחרון השמור שלהם, הפונקציה app.alert נעלמת, וסקריפטים של עיצוב אינם רצים. תוכנת Acrobat, אשר תמיד שולחת מנוע JavaScript משלה, מריצה בשמחה את אותו קובץ, וזו הסיבה שהפער נראה כל כך כמו באג בקוד שלכם כאשר הוא למעשה ברירת מחדל מכוונת בספרייה שמתחת
לרכיב PDFium הייתה שכבה היסטורית שנייה לעניין זה. עד גרסה 2.13.1 הקישור חיבר את m_pJsPlatform רק בתוך ענף האתחול של XFA, כך שמסמכי AcroForm רגילים, שהם הרוב המכריע של קובצי PDF המכילים סקריפטים, לא קיבלו פלטפורמת JavaScript כלל אפילו כאשר קובץ ה-DLL מופעל-V8 היה נוכח. גרסה 2.13.2 משכה את החיבור אל מחוץ להחלטת XFA: הפונקציה InitializeFormFill בונה כעת את טבלת IPDF_JsPlatform בכל פעם שהפונקציה V8FeaturesAvailable מחזירה True, ללא קשר לשאלה האם המסמך הוא XFA. התוצאה המעשית היא שסקריפטים ברמת המסמך, שרשראות חישוב שדות ו-app.alert מופעלים כעת גם במסמכי AcroForm רגילים
איזה קובץ V8 DLL נחוץ ל-AcroForm JavaScript ב-Delphi?
הערה: AcroForm JavaScript זקוק לקובץ בינארי של PDFium שקומפל עם מנוע ה-V8, שרכיב PDFium טוען כ-pdfium.v8.dll כאשר הדגל הגלובלי EnableV8Engine מוגדר כ-True לפני שהספרייה נטענת לראשונה. יש כאן מלכודת אחת שראוי לציין בבירור: הרכיב מזהה אוטומטית מסמכי XFA על ידי סריקה מוקדמת של סימוני /XFA ומפעיל עבורכם את EnableV8Engine, אך מסמך AcroForm רגיל עם JavaScript אינו נושא סימון XFA, ולכן לא מתבצע זיהוי אוטומטי. אם המציג שלכם אמור להריץ סקריפטים של טפסים, הגדירו את הדגל בעצמכם, פעם אחת, לפני טעינת המסמך הראשון; לאחר שקובץ pdfium.dll רגיל נטען בתהליך לא ניתן להחליף מנועים
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;
הפונקציה V8FeaturesAvailable היא הבדיקה האמינה בזמן ריצה: היא מדווחת האם הקובץ הבינארי הנטען אכן פותר את הייצואים התלויים ב-V8, כך שהבדיקה עובדת ללא קשר לאיזה קובץ DLL הותקן בסופו של דבר. אותו מנוע ואותו דגל מפעילים גם רינדור XFA דינמי; הרקע על האופן שבו זיהוי XFA והבחירה האוטומטית של V8 משפיעים זה על זה מכוסה בזיהוי טפסי XFA וחילוץ חבילות XFA עם PDFium
אירועי מארח עבור התראות, הדפסה, דואר ושליחת טפסים
כל דבר גלוי שסקריפט עושה מנותב חזרה לקוד שלכם דרך אירועי TPdf, וכל אחד מהם מוגדר כברירת מחדל לפעולה ריקה (no-op) בטוחה כאשר אינו מוקצה. האירוע OnJavaScriptAlert מקבל את ההודעה, הכותרת, סוג הכפתור והאייקון מ-app.alert ומאפשר לכם להחזיר את תוצאת הכפתור שנלחץ; האירוע OnJavaScriptResponse משרת בקשות קלט של app.response כולל דגל הסיסמה; האירועים OnJavaScriptBeep, OnJavaScriptPrint, OnJavaScriptMail ו-OnJavaScriptSubmitForm מציפים את בקשות הצפצוף, ההדפסה, הדואר ושליחת הטופס עם ערכות הפרמטרים המלאות שלהם. מציג שאינו מקצה אף אחד מאלה עדיין מרנדר ומחשב נכון; המסמך פשוט אינו יכול לפתוח דיאלוגים, להדפיס או לשלוח דבר
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;
הפרדת ברירת המחדל חשובה למידול איומים. האירועים OnJavaScriptMail ו-OnJavaScriptSubmitForm הם בסגנון הודעה בלבד: רכיב PDFium אינו מבצע שום פעולת רשת או MAPI בעצמו, כך שמסמך זדוני הקורא ל-this.submitForm או ל-this.mailDoc מול מארח לא מוכן אינו משיג דבר. המארח חייב לבחור להשתמש בזה על ידי הקצאת מטפל ומימוש התעבורה בעצמו, מה שאומר שההחלטה להעביר נתונים אל מחוץ למכונה היא תמיד שלכם, נעשית בקוד שלכם, כאשר ה-URL והנתונים בידיכם
כיצד מונעים מקובץ PDF זדוני להפעיל כתובות URL?
פעולות URI הן המקום היחיד שבו ברירת המחדל ההיסטורית הייתה פעילה ולא פסיבית, וזו הסיבה שהן קיבלו וטו ייעודי. לפני גרסה 2.13.1, קריאת החזרה FFI_DoURIActionWithKeyboardModifier הפעילה ב-Shell את ה-URI ששדה XFA סיפק, ללא תנאי, מה שהעניק למסמך זדוני את היכולת להפעיל כתובות URL שרירותיות בלחיצה. האירוע OnXfaUriAction סוגר זאת: הרכיב מתייעץ עם האירוע לפני ביצוע, ומטפל המגדיר את Handled ל-True מבטל לחלוטין את ברירת המחדל של ShellExecuteW. השארת האירוע ללא הקצאה שומרת על ההתנהגות הישנה לצורך תאימות, ולכן מציג המודע לאבטחה חייב תמיד להקצות אותו
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;
רשימת היתרים (allow-list) של הפרוטוקול היא המינימום; מציג קפדני יותר יבקש אישור מהמשתמש או ינתב הכל דרך רכיב דפדפן מאובטח משלו. אותה פילוסופיית וטו מתפרסת על פני ערכת האירועים של גרסה 2.13.1: האירועים OnXfaEmail, OnXfaHttpRequest ו-OnXfaOpenFile חושפים כל אחד את הפרמטרים הטקסטואליים של הפעולה המבוקשת בעוד שהרכיב עצמו אינו מבצע שום תעבורת רשת או קבצים. האופן שבו הגנות אלו משתלבות באסטרטגיית מסמכים לא מהימנים רחבה יותר, כולל בידוד רינדור, הוא הנושא של בניית תצוגה מקדימה מאובטחת של PDF ב-Delphi
כתיבה לשדה הממוקד עם SetFocusedFormFieldText
הפונקציה TPdf.SetFocusedFormFieldText, שנוספה בגרסה 2.13.2, היא השותפה לכתיבה של FocusedFormFieldText: היא בוחרת את התוכן הנוכחי של השדה הממוקד באמצעות FORM_SelectAllText ודורסת אותו באמצעות FORM_ReplaceSelection, בדיוק כאילו המשתמש הקליד את ההחלפה. מכיוון שהטקסט נכנס דרך נתיב העריכה האינטראקטיבי, כל סקריפט של לחיצת מקש, עיצוב וחישוב המקושר לשדה מופעל בדיוק כמו בהקלדה ידנית, מה שהופך אותה לפונקציה הבסיסית הנכונה למילוי תוכניתי במציג השומר על JavaScript פעיל. ניהול מעבר שדות ומיקוד סביבה מכוסים בניווט בשדות טפסים עם רכיב 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;
חוסר הסימטריה בשמירה המוזכר בקוד הוא גבול אמיתי. עבור שדות טקסט ושדות בחירה (combo) של AcroForm, הערך הערוך נשמר בכניסת /V של השדה בעת איבוד המיקוד ונשמר לאורך שמירה. עבור שדות טקסט של XFA, הכתיבה נוחתת רק בחוצץ העריכה שבזיכרון, מכיוון ש-PDFium אינה חושפת שום API ציבורי למיזוג ערכי ווידג'טים חזרה לתוך חבילות הנתונים של XFA; שמירת המסמך לא תשמור את השינוי. אם עריכת נתוני XFA עמידה היא הדרישה, ערכו את ה-XML של חבילת הנתונים עצמו במקום את הווידג'ט
מגבלות שקדאי להכיר לפני המשלוח
שני גבולות כנים נותרו. ראשית, ה-API של JavaScript ש-PDFium מיישמת הוא תת-קבוצה עובדת של מודל האובייקטים של Acrobat, הממוקדת בליבת סקריפטים של טפסים: גישה לשדות, אירועי חישוב ועיצוב, app.alert ו-app.response, הדפסה, דואר ובקשות שליחה. מסמכים הנשענים על אובייקטים אקזוטיים הייחודיים ל-Acrobat ידרדרו באיכותם, בדרך כלל בשקט, לכן בדקו את הטפסים בפועל שהמשתמשים שלכם מטפלים בהם במקום להניח תאימות מלאה. שנית, הפעלת V8 פירושה משלוח של pdfium.v8.dll, קובץ בינארי גדול משמעותית מהגרסה הרגילה; מציג שאינו זקוק לעולם לסקריפטים או ל-XFA יכול להישאר עם קובץ ה-DLL הקטן יותר ולהשאיר את m_pJsPlatform לא מחובר, וזו עמדת אבטחה תקפה בפני עצמה
התבנית הנובעת מכל זה פשוטה למדי: הגדירו את EnableV8Engine בהפעלה, הקצו את אירועי ההתראה והתגובה כדי שהדיאלוגים יראו מקומיים, הקצו את OnXfaUriAction ורשמו בלוג את אירועי הדואר והשליחה, והשאירו את כל השאר בברירת המחדל של פעולה ריקה עד שדרישה מסוימת תקבע אחרת. הפניית האירועים המלאה ומשטח ה-API של מילוי הטפסים מתועדים בדף המוצר של רכיב PDFium