บทความทางเทคนิค

การรัน AcroForm JavaScript ใน Delphi ด้วย PDFium Component

แบบฟอร์มที่ยอดรวมคำนวณใหม่ใน Acrobat แต่ค้างนิ่งในโปรแกรมดู Delphi ของคุณเอง มักจะไม่ใช่บั๊กของการเรนเดอร์ PDFium จะปิดใช้งาน JavaScript ทั้งหมดของเอกสาร เว้นแต่โฮสต์จะติดตั้งแพลตฟอร์ม JS และ PDFium Component จะเชื่อมต่อแพลตฟอร์มนั้นโดยอัตโนมัติตั้งแต่เวอร์ชัน 2.13.2 เป็นต้นไป ดังนั้น AcroForm JavaScript, สคริปต์ระดับเอกสาร (doc-level scripts) และการคำนวณฟิลด์จะทำงานเมื่อใดก็ตามที่โหลด DLL ที่เปิดใช้งาน V8 โฮสต์ยังคงทำหน้าที่ควบคุมผ่านชุดของเหตุการณ์ที่สามารถแสดงผล เปลี่ยนทิศทาง หรือยับยั้งทุกสิ่งที่สคริปต์พยายามดำเนินการ

ประโยคดังกล่าวนั้นช่วยแก้ไขคำถามสนับสนุนที่เราพบในหลายรูปแบบ: "ฟอร์มสั่งซื้อคำนวณยอดรวมบรรทัด in Acrobat แต่คำนวณไม่ได้ในแอปของฉัน", "app.alert ไม่เคยทำงานเลย", "ฟิลด์วันที่ไม่จัดรูปแบบตัวเอง" ทั้งหมดนี้ย้อนกลับไปที่ข้อกำหนดเดียวกัน และการทำความเข้าใจข้อกำหนดนี้มีค่าควรอ่านสิบนาที เนื่องจากข้อกำหนดดังกล่าวนี้เป็นขอบเขตความปลอดภัยของคุณกับเอกสารที่เป็นอันตรายด้วย

ทำไมการคำนวณฟอร์ม PDF จึงไม่ทำงานใน PDFium?

PDFium ปฏิบัติต่อ JavaScript ของเอกสารเป็นแบบเลือกใช้ (opt-in) อย่างเคร่งครัด: หากสมาชิก m_pJsPlatform ของสภาพแวดล้อมการกรอกแบบฟอร์มเป็น NULL สคริปต์ทุกตัวในเอกสารจะถูกข้ามไปโดยไม่มีการแจ้งเตือน ไม่มีข้อผิดพลาด ไม่มีบรรทัดบันทึกการทำงาน ไม่มีเศษเสี้ยวของการประมวลผล ฟิลด์ที่คำนวณจะเก็บค่าล่าสุดที่จัดเก็บไว้ app.alert จะหายไป สคริปต์การจัดรูปแบบจะไม่เคยทำงานเลย Acrobat ซึ่งจัดส่งเอ็นจิน JavaScript ของตัวเองด้วยเสมอ จะรันไฟล์เดียวกันนี้อย่างราบรื่น ซึ่งเป็นสาเหตุที่ทำให้ความคลาดเคลื่อนนี้ดูเหมือนบั๊กในโค้ดของคุณ ทั้งที่จริง ๆ แล้วเป็นค่าเริ่มต้นที่จงใจตั้งไว้ในไลบรารีเบื้องหลัง

PDFium Component เคยมีเลเยอร์ประวัติศาสตร์ชั้นที่สองในเรื่องนี้ สำหรับเวอร์ชัน 2.13.1 ลงไป ตัวเชื่อมข้อมูล (binding) จะแนบ m_pJsPlatform เฉพาะภายในสาขาการกำหนดค่าเริ่มต้นของ XFA เท่านั้น ดังนั้นเอกสาร AcroForm ธรรมดา ซึ่งเป็นส่วนใหญ่ของ PDF ที่มีสคริปต์ จึงไม่เคยได้รับแพลตฟอร์ม JavaScript เลย แม้ว่าจะมี DLL ที่เปิดใช้งาน V8 อยู่ก็ตาม เวอร์ชัน 2.13.2 ได้แยกการแนบออกจากขั้นตอนการตัดสินใจของ XFA: ตอนนี้ InitializeFormFill จะสร้างตาราง IPDF_JsPlatform เสมอเมื่อ V8FeaturesAvailable ส่งคืนค่า True โดยไม่ขึ้นอยู่กับว่าเอกสารนั้นเป็น XFA หรือไม่ ผลลัพธ์ในทางปฏิบัติคือสคริปต์ระดับเอกสาร ห่วงโซ่การคำนวณฟิลด์ และ app.alert จะทำงานในเอกสาร AcroForm ทั่วไปแล้วเช่นกัน

AcroForm JavaScript ต้องการ V8 DLL ตัวใดใน Delphi?

AcroForm JavaScript ต้องการไบนารี PDFium ที่คอมไพล์ด้วยเอ็นจิน V8 ซึ่ง PDFium Component จะโหลดเป็น 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 Component จะไม่ดำเนินการเครือข่ายหรือการดำเนินการ MAPI ใด ๆ ด้วยตนเอง ดังนั้นเอกสารที่เป็นอันตรายที่เรียกใช้ this.submitForm หรือ this.mailDoc บนโฮสต์ที่ไม่ได้เตรียมตัวจะส่งผลให้ไม่ได้อะไรเลย โฮสต์ต้องเลือกใช้โดยกำหนดตัวจัดการและดำเนินการส่งข้อมูลด้วยตัวเอง ซึ่งหมายความว่าการตัดสินใจย้ายข้อมูลออกนอกเครื่องเป็นของคุณเสมอ โดยกำหนดในโค้ดของคุณ พร้อม URL และเพย์โหลดข้อมูล

คุณจะหยุด PDF ที่เป็นอันตรายจากการเปิดใช้ URL ได้อย่างไร?

การกระทำ URI เป็นที่เดียวที่พฤติกรรมเริ่มต้นในอดีตเป็นแบบเชิงรุกแทนที่จะเป็นเชิงรับ และเป็นเหตุผลว่าทำไมพวกเขาจึงได้รับสิทธิ์ยับยั้งโดยเฉพาะ ก่อนเวอร์ชัน 2.13.1 คอลแบ็ก FFI_DoURIActionWithKeyboardModifier จะสั่งให้เชลล์รัน 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) บนโครงร่าง URL คือระดับขั้นต่ำ; โปรแกรมดูที่เข้มงวดกว่าจะยืนยันกับผู้ใช้หรือเปลี่ยนเส้นทางทุกอย่างผ่านส่วนประกอบเบราว์เซอร์แซนด์บ็อกซ์ของตัวเอง ปรัชญาการยับยั้งแบบเดียวกันนี้ครอบคลุมถึงชุดเหตุการณ์ v2.13.1: OnXfaEmail, OnXfaHttpRequest, และ OnXfaOpenFile แต่ละรายการจะเปิดเผยพารามิเตอร์ที่เป็นข้อความของแอ็กชันที่ร้องขอ ในขณะที่ตัวคอมโพเนนต์เองจะไม่ดำเนินการรับส่งข้อมูลเครือข่ายหรือไฟล์ วิธีการที่มาตรการป้องกันเหล่านี้สอดคล้องกับกลยุทธ์เอกสารที่ไม่น่าเชื่อถือในวงกว้าง รวมถึงการแยกส่วนการเรนเดอร์ เป็นหัวข้อของการสร้างโปรแกรมแสดงตัวอย่าง PDF ที่ปลอดภัยใน Delphi

การเขียนลงในฟิลด์ที่โฟกัสด้วย SetFocusedFormFieldText

TPdf.SetFocusedFormFieldText ซึ่งเพิ่มเข้ามาในเวอร์ชัน 2.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; การบันทึกเอกสารจะไม่นำการเปลี่ยนแปลงนี้ไปด้วย หากความต้องการคือการแก้ไขข้อมูล XFA ที่ทนทาน ให้แก้ไขชุดข้อมูล XML ด้วยตนเองแทนที่จะแก้ไขวิจิท

ข้อจำกัดที่ควรทราบก่อนจัดส่งใช้งาน

ข้อจำกัดที่แท้จริงสองประการยังคงอยู่ ประการแรก JavaScript API ที่ PDFium ติดตั้งนั้นเป็นส่วนย่อยที่ทำงานได้ของโมเดลวัตถุ Acrobat โดยเน้นที่แกนหลักการเขียนสคริปต์ฟอร์ม: การเข้าถึงฟิลด์ เหตุการณ์การคำนวณและการจัดรูปแบบ app.alert และ app.response การพิมพ์ อีเมล และคำขอส่งแบบฟอร์ม เอกสารที่อิงวัตถุแปลกใหม่เฉพาะของ Acrobat จะมีประสิทธิภาพลดลง มักเกิดอย่างเงียบเชียบ ดังนั้นโปรดทดสอบแบบฟอร์มจริงที่ผู้ใช้ของคุณใช้แทนที่จะอนุมานว่าเหมือนกันหมด ประการที่สอง การเปิดใช้งาน V8 หมายถึงการจัดส่ง pdfium.v8.dll ซึ่งเป็นไบนารีที่มีขนาดใหญ่กว่ารุ่นธรรมดาอย่างมาก; โปรแกรมดูที่ไม่เคยต้องการการเขียนสคริปต์หรือ XFA สามารถเลือกใช้ DLL ขนาดเล็กกว่าและปล่อยให้ m_pJsPlatform ว่างเปล่าไว้ ซึ่งเป็นทัศนคติความปลอดภัยที่ถูกต้องในตัวเอง

รูปแบบที่ออกมาจากกระบวนการนี้เป็นเรื่องเรียบง่าย: กำหนดค่า EnableV8Engine เมื่อเริ่มต้นระบบ มอบหมายเหตุการณ์การแจ้งเตือนและการตอบกลับเพื่อให้กล่องโต้ตอบดูเหมือนของระบบแท้ มอบหมาย OnXfaUriAction และบันทึกการส่งเมลและส่งแบบฟอร์ม และปล่อยสิ่งอื่น ๆ ไว้ที่ค่าเริ่มต้นแบบไม่มีผลใด ๆ จนกว่าข้อกำหนดจะระบุไว้เป็นอย่างอื่น ข้อมูลอ้างอิงเหตุการณ์ที่สมบูรณ์และส่วนการกรอกแบบฟอร์มมีระบุไว้บนหน้าผลิตภัณฑ์ PDFium Component