مقاله فنی

اجرای جاوااسکریپت AcroForm در دلفی با PDFium Component

فرمی که مجموع آن در Acrobat دوباره محاسبه می‌شود اما در نمایشگر دلفی خودتان به صورت منجمد باقی می‌ماند، تقریباً هرگز یک باگ رندرینگ نیست. کتابخانه PDFium تمام جاوااسکریپت‌های سند را غیرفعال می‌کند مگر اینکه میزبان یک پلتفرم JS به آن متصل کند، و کامپوننت PDFium این پلتفرم را به طور خودکار از نسخه ۲.۱۳.۲ به بعد متصل می‌نماید، بنابراین جاوااسکریپت AcroForm، اسکریپت‌های سطح سند و محاسبات فیلدها هر زمان که DLL مجهز به V8 بارگذاری شود اجرا می‌شوند. میزبان از طریق مجموعه‌ای از رویدادها که می‌توانند هر کاری را که اسکریپت تلاش می‌کند انجام دهد نمایش دهند، هدایت کنند یا وتو نمایند، در کنترل باقی می‌ماند

این یک جمله، یک سوال پشتیبانی را که در چندین شکل دیده‌ایم حل می‌کند: "فرم سفارش من مجموع اقلام سطرها را در Acrobat محاسبه می‌کند اما در برنامه من نه"، "متد app.alert هرگز اجرا نمی‌شود"، "فیلد تاریخ خود را فرمت نمی‌کند". همه آنها به یک قرارداد بازمی‌گردند، و درک آن ارزش ده دقیقه زمان را دارد زیرا همین قرارداد مرز امنیتی شما در برابر اسناد خصمانه است

چرا محاسبات فرم PDF در PDFium کار نمی‌کنند؟

کتابخانه PDFium با جاوااسکریپت سند کاملاً به عنوان یک مورد انتخابی برخورد می‌کند: اگر عضو m_pJsPlatform از محیط پر کردن فرم برابر NULL باشد، از تک‌تک اسکریپت‌ها در سند بی سر و صدا عبور می‌شود. بدون هیچ خطا، بدون لاگ و بدون اجرای جزئی. فیلدهای محاسباتی آخرین مقدار ذخیره شده خود را نگه می‌دارند، app.alert ناپدید می‌شود و اسکریپت‌های فرمت هرگز اجرا نمی‌گردند. نرم‌افزار Acrobat که همیشه با موتور جاوااسکریپت خود ارائه می‌شود، همان فایل را به خوبی اجرا می‌کند، به همین دلیل است که این تفاوت بسیار شبیه به یک باگ در کد شما به نظر می‌رسد در حالی که در واقع یک پیش‌فرض تعمدی در کتابخانه زیرین است

کامپوننت PDFium یک لایه تاریخی دوم نیز برای این موضوع داشت. تا نسخه ۲.۱۳.۱، اتصال فقط m_pJsPlatform را در داخل شاخه مقداردهی اولیه XFA متصل می‌کرد، بنابراین اسناد ساده AcroForm، که اکثریت قاطع PDFهای اسکریپت‌شده را تشکیل می‌دهند، حتی در صورت حضور DLL مجهز به V8 اصلاً پلتفرم جاوااسکریپت را دریافت نمی‌کردند. نسخه ۲.۱۳.۲ اتصال را از تصمیم XFA خارج کرد: متد InitializeFormFill اکنون هر زمان که V8FeaturesAvailable مقدار True را برگرداند، جدول IPDF_JsPlatform را می‌سازد، بدون توجه به اینکه آیا سند XFA است یا خیر. نتیجه عملی این است که اسکریپت‌های سطح سند، زنجیره‌های محاسبه فیلد و متد app.alert اکنون در اسناد معمولی AcroForm نیز اجرا می‌شوند

جاوااسکریپت AcroForm به کدام DLL موتور V8 در دلفی نیاز دارد؟

جاوااسکریپت AcroForm به یک باینری PDFium نیاز دارد که با موتور V8 کامپایل شده باشد، که کامپوننت PDFium آن را با نام pdfium.v8.dll بارگذاری می‌کند در صورتی که فلگ سراسری EnableV8Engine قبل از اولین بارگذاری کتابخانه، روی True تنظیم شده باشد. در اینجا یک تله وجود دارد که ارزش بیان صریح را دارد: کامپوننت اسناد XFA را با اسکن اولیه برای نشانگرهای /XFA به طور خودکار شناسایی کرده و EnableV8Engine را برای شما تغییر می‌دهد، اما یک سند ساده AcroForm دارای جاوااسکریپت، نشانگر XFA به همراه ندارد، بنابراین شناسایی خودکار رخ نمی‌دهد. اگر نمایشگر شما باید اسکریپت‌های فرم را اجرا کند، خودتان قبل از بارگذاری اولین سند، فلگ را یک بار تنظیم کنید؛ پس از بارگذاری pdfium.dll ساده، پروسه نمی‌تواند موتورها را تعویض کند

متد V8FeaturesAvailable بررسی واقعی در زمان اجرا است: این متد گزارش می‌دهد که آیا باینری بارگذاری‌شده در واقع متدهای وابسته به V8 را حل می‌کند یا خیر، بنابراین بررسی بدون توجه به اینکه نصب‌کننده کدام DLL را مستقر کرده کار می‌کند. همان موتور و همان فلگ، رندر پویا XFA را نیز تأمین می‌کنند؛ سابقه چگونگی تعامل شناسایی XFA و انتخاب خودکار V8 در مقاله شناسایی فرم‌های XFA و استخراج پکت‌های XFA با استفاده از PDFium پوشش داده شده است

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;

رویدادهای میزبان برای هشدارها، چاپ، ایمیل و ارسال فرم

هر کار قابل مشاهده‌ای که یک اسکریپت انجام می‌دهد، از طریق رویدادهای TPdf به کد شما بازمی‌گردد و در صورت عدم اختصاص، پیش‌فرض هر یک از آنها یک عملیات امن بدون کارکرد است. رویداد OnJavaScriptAlert پیام، عنوان، نوع دکمه و آیکون را از app.alert دریافت می‌کند و به شما اجازه می‌دهد نتیجه دکمه فشرده‌شده را برگردانید؛ OnJavaScriptResponse به اعلان‌های ورودی app.response از جمله پرچم رمز عبور سرویس می‌دهد؛ و رویدادهای OnJavaScriptBeep، OnJavaScriptPrint، OnJavaScriptMail و OnJavaScriptSubmitForm درخواست‌های بوق، چاپ، ایمیل و ارسال را با مجموعه کامل پارامترهایشان نشان می‌دهند. نمایشگری که هیچ‌کدام از این رویدادها را اختصاص نمی‌دهد همچنان رندر می‌کند و به درستی محاسبه می‌نماید؛ سند به سادگی نمی‌تواند دیالوگ‌ها را باز کند، چاپ کند یا چیزی ارسال نماید

تقسیم رفتار پیش‌فرض برای مدل‌سازی تهدید اهمیت دارد. رویدادهای OnJavaScriptMail و OnJavaScriptSubmitForm به سبک اعلان هستند: کامپوننت PDFium هیچ عملیات شبکه یا MAPI را به تنهایی انجام نمی‌دهد، بنابراین یک سند مخرب که متدهای this.submitForm یا this.mailDoc را در برابر میزبان آماده‌نشده فراخوانی می‌کند، دقیقاً هیچ کاری را به سرانجام نمی‌رساند. میزبان باید با اختصاص یک هندلر و پیاده‌سازی انتقال توسط خودش موافقت کند، به این معنی که تصمیم برای انتقال داده‌ها به خارج از سیستم همیشه با شماست، در کد شما گرفته می‌شود، در حالی که URL و داده‌ها را در دست دارید

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;

چگونه از راه‌اندازی URLها توسط یک PDF مخرب جلوگیری می‌کنید؟

عملیات URI تنها جایی است که پیش‌فرض تاریخی به جای غیرفعال بودن، فعال بود و به همین دلیل یک حق وتوی اختصاصی دریافت کرد. قبل از نسخه ۲.۱۳.۱، کال‌بک FFI_DoURIActionWithKeyboardModifier بدون قید و شرط هر URI را که یک فیلد XFA ارائه می‌داد، اجرا می‌کرد، که به یک سند مخرب توانایی راه‌اندازی URLهای دلخواه را با کلیک می‌داد. رویداد OnXfaUriAction آن را می‌بندد: کامپوننت قبل از اجرا با رویداد مشورت می‌کند و هندلری که Handled را روی True تنظیم کند، پیش‌فرض ShellExecuteW را کاملاً سرکوب می‌کند. رها کردن رویداد به صورت اختصاص‌نیافته، رفتار قدیمی را برای سازگاری حفظ می‌کند، بنابراین یک نمایشگر حساس به امنیت باید همیشه آن را اختصاص دهد

یک لیست مجاز روی طرحواره (scheme) حداقل کار است؛ یک نمایشگر سخت‌گیرتر از کاربر تأییدیه می‌گیرد یا همه چیز را از طریق کامپوننت مرورگر سندباکس شده خود هدایت می‌کند. همان فلسفه وتو در سراسر مجموعه رویدادهای نسخه ۲.۱۳.۱ گسترش می‌یابد: رویدادهای OnXfaEmail، OnXfaHttpRequest و OnXfaOpenFile هر کدام پارامترهای متنی عملیات درخواستی را در معرض دید قرار می‌دهند در حالی که خود کامپوننت هیچ کار انتقال شبکه یا فایل را انجام نمی‌دهد. چگونگی تناسب این محافظ‌ها در یک استراتژی گسترده‌تر اسناد نامعتبر، از جمله ایزوله‌سازی رندر، موضوع مقاله ساخت پیش‌نمایش امن PDF در دلفی است

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;

نوشتن در فیلد فوکوس شده با SetFocusedFormFieldText

متد TPdf.SetFocusedFormFieldText که در نسخه ۲.۱۳.۲ اضافه شد، همراه بخش نوشتن برای FocusedFormFieldText است: این متد محتوای فعلی فیلد فوکوس شده را از طریق FORM_SelectAllText انتخاب می‌کند و آن را از طریق FORM_ReplaceSelection رونویسی می‌نماید، دقیقاً به گونه‌ای که کاربر جایگزین را تایپ کرده باشد. از آنجا که متن از طریق مسیر ویرایش تعاملی وارد می‌شود، هرگونه اسکریپت فشرده‌شدن کلید، فرمت و محاسبه متصل به فیلد به همان روشی که برای ورودی دستی انجام می‌شود، اجرا می‌گردد، که این ویژگی آن را به گزینه مناسبی برای پر کردن برنامه‌نویسی در نمایشگری که جاوااسکریپت را زنده نگه می‌دارد تبدیل می‌کند. ناوبری فیلد و مدیریت فوکوس در اطراف آن در مقاله ناوبری فیلدهای فرم با استفاده از PDFium Component پوشش داده شده است

عدم تقارن ماندگاری در آن توضیح یک مرز واقعی است، نه صرفاً یک تذکر. برای فیلدهای متنی و کامبو در AcroForm، مقدار ویرایش‌شده با از دست رفتن فوکوس به ورودی /V فیلد منتقل می‌شود و در حین ذخیره پایداری دارد. برای فیلدهای متنی XFA، نوشتن فقط در بافر ویرایش درون حافظه می‌نشیند، زیرا PDFium هیچ API عمومی را برای تطبیق مقادیر ویجت به پکت مجموعه داده‌های XFA ارائه می‌دهد؛ ذخیره سند تغییر را به همراه نخواهد داشت. اگر ویرایش پایدار داده‌های XFA مورد نیاز است، به جای ویجت، خود XML مجموعه داده‌ها را ویرایش کنید

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;

محدودیت‌هایی که ارزش دارد قبل از ارسال بدانید

دو مرز صادقانه باقی می‌ماند. اول اینکه، جاوااسکریپت API که PDFium پیاده‌سازی می‌کند یک زیرمجموعه کارآمد از مدل شیء Acrobat است که بر روی هسته اسکریپت‌نویسی فرم متمرکز شده است: دسترسی به فیلد، رویدادهای محاسبه و فرمت، متدهای app.alert و app.response، و درخواست‌های چاپ، ایمیل و ارسال. اسنادی که به اشیاء غیرمعمول و انحصاری Acrobat متکی هستند، معمولاً بی سروصدا تنزل کارکرد خواهند داشت، بنابراین فرم‌های واقعی را که کاربران شما مدیریت می‌کنند تست کنید به جای اینکه فرض را بر برابری بگذارید. دوم اینکه، فعال کردن V8 به معنای ارسال فایل pdfium.v8.dll است که یک باینری بسیار بزرگتر از بیلد ساده است; نمایشگری که هرگز به اسکریپت‌نویسی یا XFA نیاز ندارد، می‌تواند به طور قانونی DLL کوچکتری را نگه دارد و m_pJsPlatform را غیرمتصل رها کند، که این خود یک موضع امنیتی معتبر است

الگویی که از همه اینها حاصل می‌شود لذت‌بخش است: تنظیم EnableV8Engine در هنگام استارت‌آپ، اختصاص رویدادهای هشدار و پاسخ به طوری که دیالوگ‌ها بومی به نظر برسند، اختصاص رویداد OnXfaUriAction و لاگ کردن رویدادهای ایمیل و ارسال، و رها کردن بقیه موارد در پیش‌فرض بدون کارکرد خود تا زمانی که نیازی بر خلاف آن اعلام شود. مرجع کامل رویدادها و سطح API پر کردن فرم در صفحه محصول PDFium Component مستند شده است