Bài viết Kỹ thuật

Chạy AcroForm JavaScript trong Delphi với PDFium Component

Một biểu mẫu có tổng số tiền tự động tính toán lại trong Acrobat nhưng lại đứng im trong trình xem Delphi của bạn hầu như không bao giờ là lỗi dựng hình. PDFium vô hiệu hóa tất cả JavaScript của tài liệu trừ khi host đính kèm một nền tảng JS, và PDFium Component sẽ tự động kết nối nền tảng đó từ phiên bản v2.13.2 trở đi, do đó AcroForm JavaScript, các script cấp tài liệu, và các phép tính toán trường sẽ thực thi bất cứ khi nào file DLL hỗ trợ V8 được tải. Host vẫn giữ quyền kiểm soát thông qua một bộ sự kiện có thể hiển thị, chuyển hướng, hoặc phủ quyết mọi thứ mà script cố gắng thực hiện

Câu giải thích đó giải quyết một câu hỏi hỗ trợ mà chúng tôi đã gặp ở nhiều dạng: "biểu mẫu đơn hàng của tôi tính toán tổng số tiền trong Acrobat nhưng không hoạt động trong ứng dụng của tôi", "app.alert không bao giờ kích hoạt", "trường ngày tháng không tự định dạng". Tất cả đều bắt nguồn từ cùng một hợp đồng (contract), và việc hiểu rõ nó rất xứng đáng vì cùng một hợp đồng đó cũng chính là ranh giới bảo mật của bạn trước các tài liệu độc hại

Tại sao các phép tính toán biểu mẫu PDF không hoạt động trong PDFium?

PDFium coi JavaScript của tài liệu là strictly opt-in: nếu thành viên m_pJsPlatform của môi trường form-fill là NULL, mọi script trong tài liệu sẽ âm thầm bị bỏ qua. Không có lỗi, không có dòng nhật ký, không có thực thi một phần. Các trường được tính toán sẽ giữ nguyên giá trị được lưu trữ cuối cùng của chúng, app.alert biến mất, các script định dạng không bao giờ chạy. Acrobat, vốn luôn đi kèm engine JavaScript riêng của nó, chạy file đó bình thường, đó là lý do tại sao sự khác biệt này trông rất giống một lỗi trong code của bạn khi thực chất nó là một mặc định có chủ ý của thư viện bên dưới

PDFium Component từng có lớp lịch sử thứ hai đối với vấn đề này. Cho đến phiên bản v2.13.1, liên kết chỉ đính kèm m_pJsPlatform bên trong nhánh khởi tạo XFA, vì vậy các tài liệu AcroForm thông thường, chiếm đại đa số các file PDF có script, không bao giờ nhận được nền tảng JavaScript ngay cả khi có DLL hỗ trợ V8. Phiên bản v2.13.2 đã tách phần đính kèm ra khỏi quyết định XFA: InitializeFormFill giờ đây xây dựng bảng IPDF_JsPlatform bất cứ khi nào V8FeaturesAvailable trả về True, không phụ thuộc vào việc tài liệu đó có phải là XFA hay không. Kết quả thực tế là các script cấp tài liệu, chuỗi tính toán trường, và app.alert giờ đây cũng thực thi trong các tài liệu AcroForm thông thường

File DLL V8 nào AcroForm JavaScript need trong Delphi?

AcroForm JavaScript cần một file binary PDFium được biên dịch với engine V8, file này được PDFium Component tải dưới dạng pdfium.v8.dll khi cờ toàn cục EnableV8Engine là True trước khi thư viện được tải lần đầu tiên. Có một cái bẫy ở đây cần nêu rõ ràng: component tự động phát hiện các tài liệu XFA bằng cách quét trước các ký tự /XFA và lật cờ EnableV8Engine cho bạn, nhưng một tài liệu AcroForm thông thường có JavaScript không mang ký tự XFA nào, do đó không có sự tự động phát hiện nào xảy ra. Nếu trình xem của bạn cần chạy các script biểu mẫu, hãy tự thiết lập cờ đó một lần trước khi tài liệu đầu tiên được tải; sau khi một file pdfium.dll thông thường đã được tải vào process, nó không thể chuyển đổi engine

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 là lệnh thăm dò runtime chuẩn xác: nó báo cáo liệu binary được tải có thực sự phân giải các export phụ thuộc V8 hay không, vì vậy kiểm tra này hoạt động bất kể file DLL nào được cài đặt. Engine tương tự và cờ tương tự cũng cung cấp sức mạnh cho việc dựng hình XFA động; thông tin nền tảng về cách phát hiện XFA và tự động lựa chọn V8 tương tác với nhau được trình bày trong phát hiện các biểu mẫu XFA và trích xuất các gói XFA với PDFium

Các sự kiện host cho alert, in ấn, email, và gửi biểu mẫu

Mọi hoạt động trực quan mà một script thực hiện đều định tuyến trở lại code của bạn thông qua các sự kiện TPdf, và mỗi sự kiện trong số đó đều mặc định ở trạng thái no-op an toàn khi không được gán. OnJavaScriptAlert nhận thông điệp, tiêu đề, loại nút bấm và icon từ app.alert và cho phép bạn trả về kết quả nút bấm được nhấn; OnJavaScriptResponse phục vụ các thông báo nhập liệu app.response bao gồm cả cờ password; OnJavaScriptBeep, OnJavaScriptPrint, OnJavaScriptMail, và OnJavaScriptSubmitForm hiển thị các yêu cầu tiếng bíp, in ấn, gửi thư, và gửi biểu mẫu với đầy đủ các bộ tham số của chúng. Một trình xem không gán bất kỳ sự kiện nào trong số này vẫn dựng hình và tính toán chính xác; tài liệu chỉ đơn giản là không thể mở các hộp thoại, in ấn, hoặc gửi đi bất kỳ thứ gì

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;

Sự phân chia hành vi mặc định rất quan trọng đối với việc lập mô hình mối đe dọa (threat modeling). OnJavaScriptMailOnJavaScriptSubmitForm thuộc dạng thông báo: PDFium Component không tự thực hiện hành động mạng hay MAPI nào, do đó một tài liệu độc hại gọi this.submitForm hoặc this.mailDoc đối với một host chưa chuẩn bị sẽ không thực hiện được điều gì cả. Host phải chọn tham gia bằng cách gán một trình xử lý và tự triển khai việc vận chuyển, có nghĩa là quyết định di chuyển dữ liệu ra khỏi máy luôn là của bạn, được đưa ra trong code của bạn, với URL và payload nằm trong tay

Làm thế nào để ngăn chặn một file PDF độc hại kích hoạt các URL?

Các hành động URI là nơi duy nhất mà hành vi mặc định trong lịch sử hoạt động ở trạng thái chủ động thay vì thụ động, và đó là lý do tại sao chúng có một sự phủ quyết chuyên dụng. Trước phiên bản v2.13.1, callback FFI_DoURIActionWithKeyboardModifier thực thi shell bất kỳ URI nào mà một trường XFA cung cấp, một cách vô điều kiện, điều này trao cho một tài liệu độc hại khả năng khởi chạy các URL tùy ý khi được nhấp vào. OnXfaUriAction đóng lỗ hổng đó: component tham khảo sự kiện trước khi thực thi, và một trình xử lý thiết lập Handled thành True sẽ chặn hoàn toàn mặc định ShellExecuteW. Việc để sự kiện không được gán sẽ bảo toàn hành vi cũ để tương thích, vì vậy một trình xem chú trọng bảo mật nên luôn gán sự kiện này

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;

Một danh sách cho phép (allow-list) trên scheme là mức tối thiểu; một trình xem nghiêm ngặt hơn sẽ xác nhận với người dùng hoặc định tuyến mọi thứ qua component trình duyệt được đóng hộp cát (sandboxed browser component) của riêng nó. Triết lý phủ quyết tương tự mở rộng trên bộ sự kiện v2.13.1: OnXfaEmail, OnXfaHttpRequest, và OnXfaOpenFile mỗi sự kiện hiển thị các tham số văn bản của hành động được yêu cầu trong khi bản thân component không thực hiện việc vận chuyển qua mạng hay file. Cách các biện pháp bảo vệ này ăn khớp vào một chiến lược tài liệu không tin cậy rộng hơn, bao gồm cả cách ly dựng hình, là chủ đề của xây dựng một bản xem trước PDF an toàn trong Delphi

Ghi vào trường đang được focus với SetFocusedFormFieldText

TPdf.SetFocusedFormFieldText, được thêm vào trong phiên bản v2.13.2, là người bạn đồng hành ghi bên cạnh FocusedFormFieldText: nó chọn toàn bộ nội dung hiện tại của trường đang focus thông qua FORM_SelectAllText và ghi đè lên đó thông qua FORM_ReplaceSelection, chính xác như thể người dùng đã nhập phần thay thế. Vì văn bản đi qua đường dẫn chỉnh sửa tương tác, mọi script keystroke, định dạng, và tính toán được gắn với trường sẽ kích hoạt theo cách tương tự như đối với việc nhập liệu thủ công, làm cho nó trở thành mã lệnh nguyên thủy chính xác cho việc điền bằng lập trình trong một trình xem giữ cho JavaScript hoạt động. Việc duyệt trường và quản lý focus xung quanh nó được trình bày trong điều hướng trường biểu mẫu với 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;

Sự bất đối xứng về tính bền vững (persistence asymmetry) trong ghi chú đó là một giới hạn thực tế, không phải là một lưu ý để cho có. Đối với các trường văn bản và combo AcroForm, giá trị đã chỉnh sửa sẽ được commit vào mục /V của trường khi mất focus và tồn tại lâu dài qua các lệnh lưu. Đối với các trường văn bản XFA, việc ghi chỉ nằm trong bộ đệm chỉnh sửa trong bộ nhớ, vì PDFium không hiển thị API công khai nào để đối chiếu các giá trị widget trở lại các gói dataset XFA; việc lưu tài liệu sẽ không mang theo thay đổi này. Nếu việc chỉnh sửa dữ liệu XFA lâu dài là yêu cầu bắt buộc, hãy chỉnh sửa XML của chính các dataset thay vì chỉnh sửa widget

Những giới hạn đáng biết trước khi bạn phân phối

Hai giới hạn thực tế vẫn còn. Thứ nhất, API JavaScript mà PDFium triển khai là một tập hợp con hoạt động của mô hình đối tượng Acrobat, tập trung vào phần cốt lõi của việc script biểu mẫu: truy cập trường, các sự kiện tính toán và định dạng, app.alertapp.response, các yêu cầu in ấn, gửi thư, và gửi biểu mẫu. Các tài liệu dựa trên các đối tượng lạ chỉ có trong Acrobat sẽ bị hạ cấp, thường là âm thầm, vì vậy hãy kiểm thử các biểu mẫu thực tế mà người dùng của bạn xử lý thay vì giả định tính tương đương. Thứ hai, việc bật V8 có nghĩa là phải phân phối pdfium.v8.dll, một file binary lớn hơn đáng kể so với bản dựng thông thường; một trình xem không bao giờ cần viết script hoặc XFA có thể chọn giữ file DLL nhỏ hơn và để m_pJsPlatform không đính kèm, đây là một tư thế bảo mật hợp lệ

Mô hình rút ra từ tất cả những điều này khá đơn giản: thiết lập EnableV8Engine khi khởi động, gán các sự kiện alert và response để các hộp thoại trông như gốc, gán OnXfaUriAction và ghi nhật ký kiểm toán (audit-log) các sự kiện mail và submit, và để mọi thứ khác ở mặc định no-op của nó cho đến khi có yêu cầu khác chỉ định. Tài liệu tham khảo sự kiện đầy đủ và bề mặt API điền biểu mẫu được tài liệu hóa trên trang sản phẩm PDFium Component