Форма, підсумки якої перераховуються в Acrobat, але залишаються незмінними у вашому власному переглядачі на Delphi — це майже ніколи не є помилкою рендерингу. PDFium вимикає весь JavaScript документа, якщо хост-додаток не підключить платформу JS, а PDFium Component підключає цю платформу автоматично починаючи з версії v2.13.2, тож JavaScript в AcroForm, скрипти на рівні документа та обчислення полів виконуються щоразу, коли завантажується DLL-бібліотека з підтримкою V8. При цьому хост зберігає контроль за допомогою набору подій, які можуть відображати, перенаправляти або забороняти будь-які дії, які намагається виконати скрипт
Це пояснює низку питань підтримки, які ми часто чуємо: «моя форма замовлення обчислює підсумки в Acrobat, але не в моєму додатку», «метод app.alert ніколи не спрацьовує», «поле дати не форматується автоматично». Усі вони пов'язані з одним і тим самим механізмом, і знайомство з ним варте десяти хвилин вашого часу, оскільки цей механізм є також вашою межею безпеки проти шкідливих документів
Чому обчислення у формах PDF не працюють у PDFium?
PDFium розглядає JavaScript документа як опціональну функцію (opt-in): якщо член m_pJsPlatform середовища заповнення форм дорівнює NULL, кожен скрипт у документі просто мовчки ігнорується. Жодних помилок, записів у логах чи часткового виконання. Обчислювані поля зберігають своє останнє записане значення, виклик app.alert не працює, а скрипти форматування не запускаються. Acrobat, який завжди поставляється з власним движком JavaScript, без проблем запускає цей самий файл, саме тому така різниця виглядає як помилка у вашому коді, хоча насправді це свідоме налаштування за замовчуванням у самій бібліотеці
У PDFium Component була і друга, історична сторона цього питання. До версії v2.13.1 зв'язування підключало m_pJsPlatform лише всередині гілки ініціалізації XFA, тому звичайні документи AcroForm (які становлять переважну більшість скриптованих PDF) взагалі не отримували платформу JavaScript, навіть коли була присутня DLL з підтримкою V8. У версії v2.13.2 це підключення було винесено за межі логіки XFA: метод InitializeFormFill тепер створює таблицю IPDF_JsPlatform щоразу, коли функція V8FeaturesAvailable повертає True, незалежно від того, чи є документ формою XFA. Практичний результат полягає в тому, що скрипти на рівні документа, ланцюжки обчислень полів та метод app.alert тепер виконуються і у звичайних документах AcroForm
Яка бібліотека V8 DLL потрібна для роботи JavaScript в AcroForm у Delphi?
Для роботи JavaScript в AcroForm потрібен бінарний файл PDFium, скомпільований із движком V8, який PDFium Component завантажує як pdfium.v8.dll, якщо глобальний прапорець EnableV8Engine встановлено в значення True перед першим завантаженням бібліотеки. Тут є одна пастка, яку варто описати детально: компонент автоматично виявляє документи XFA шляхом попереднього сканування на наявність маркерів /XFA і самостійно вмикає EnableV8Engine, але звичайний документ AcroForm із JavaScript не містить такого маркера, тому автовизначення не спрацьовує. Якщо ваш переглядач має виконувати скрипти форм, встановіть цей прапорець самостійно один раз перед завантаженням першого документа; після того, як у процес буде завантажено звичайний файл 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, і за замовчуванням (якщо обробники не призначені) кожна з них просто ігнорується. Подія 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 Component не виконує жодних мережевих дій чи викликів MAPI, тому шкідливий документ, який викликає this.submitForm або this.mailDoc у непідготовленому додатку, нічого не зможе зробити. Хост повинен явно дозволити це, призначивши обробник та реалізувавши передачу даних самостійно, а отже, рішення про надсилання даних за межі комп'ютера завжди залишається за вами, у вашому коді, з повною інформацією про URL та дані для надсилання
Як заборонити шкідливому PDF відкривати URL-адреси?
Дії з URI — це єдине місце, де поведінка за замовчуванням історично була активною, а не пасивною, і саме тому для них було впроваджено окреме право вето. До версії v2.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;
Білий список схем — це мінімальна вимога; суворіший переглядач запитуватиме підтвердження у користувача або спрямовуватиме всі запити у власний ізольований (sandboxed) компонент браузера. Та сама філософія блокування поширюється на весь набір подій версії v2.13.1: OnXfaEmail, OnXfaHttpRequest, та OnXfaOpenFile передають текстові параметри запитуваної дії, тоді як сам компонент не здійснює мережевих пересилань чи відкриття файлів. Про те, як ці засоби захисту вписуються в загальну стратегію роботи з ненадійними документами (включаючи ізоляцію рендерингу), читайте у побудові безпечного перегляду PDF у Delphi
Запис у сфокусоване поле за допомогою SetFocusedFormFieldText
Метод TPdf.SetFocusedFormFieldText, доданий у версії v2.13.2, є аналогом властивості читання FocusedFormFieldText: він виділяє поточний вміст сфокусованого поля через FORM_SelectAllText і перезаписує його за допомогою FORM_ReplaceSelection, точно так само, як ніби користувач сам ввів текст із клавіатури. Оскільки текст потрапляє через шлях інтерактивного редагування, будь-які скрипти натискання клавіш, форматування та обчислення, прив'язані до поля, спрацьовують так само, як і при ручному введенні, що робить його правильним інструментом для програмного заповнення у переглядачі з активним JavaScript. Теми обходу полів та керування фокусом описані в навігації по полях форми за допомогою 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;
Асиметрія збереження даних у цьому коментарі є реальним обмеженням, а не просто застереженням. Для текстових полів та комбобоксів AcroForm змінене значення записується в розділ поля /V при втраті фокусу і зберігається при записі файлу. Для текстових полів XFA запис потрапляє лише в буфер редагування в оперативній пам'яті, оскільки PDFium не надає відкритих методів API для синхронізації значень віджетів назад у пакет даних XFA datasets; збереження документа не перенесе ці зміни. Якщо вам потрібне надійне редагування даних XFA, змінюйте сам XML-код datasets, а не віджет
Обмеження, які варто знати перед випуском програми
Залишаються два реальних обмеження. По-перше, API JavaScript, який реалізує PDFium, є лише робочою підмножиною об'єктної моделі Acrobat, орієнтованою на базові потреби скриптів форм: доступ до полів, події обчислення та форматування, методи app.alert та app.response, запити друку, пошти та надсилання даних. Документи, які покладаються на нетипові об'єкти, що є лише в Acrobat, не працюватимуть (зазвичай мовчки), тому тестуйте реальні форми, з якими працюють ваші користувачі, а не сподівайтеся на повну сумісність. По-друге, увімкнення движка V8 означає необхідність поставляти файл pdfium.v8.dll, який є значно більшим за звичайну збірку; переглядач, якому ніколи не потрібні скрипти чи форми XFA, може використовувати звичайну меншу DLL-бібліотеку та залишати m_pJsPlatform неактивним, що само по собі є надійною позицією з точки зору безпеки
Загальний шаблон для налаштування є досить простим: увімкнути EnableV8Engine при запуску, призначити обробники подій сповіщень (alert) та відповідей (response), щоб діалоги виглядали рідними, призначити OnXfaUriAction та логувати події надсилання пошти і форм, залишаючи решту обробників за замовчуванням, поки не виникне інша потреба. Повний опис подій та інтерфейс API заповнення форм задокументовані на сторінці продукту PDFium Component