Τεχνικό Άρθρο

Εκτέλεση AcroForm JavaScript στο Delphi με το PDFium Component

Μια φόρμα της οποίας τα σύνολα επανυπολογίζονται στο Acrobat αλλά παραμένουν παγωμένα στο δικό σας πρόγραμμα προβολής Delphi δεν είναι σχεδόν ποτέ σφάλμα απόδοσης. Το PDFium απενεργοποιεί όλη τη JavaScript του εγγράφου εκτός εάν ο οικοδεσπότης (host) συνδέσει μια πλατφόρμα JS, και το PDFium Component συνδέει αυτήν την πλατφόρμα αυτόματα από την έκδοση 2.13.2 και μετά. Έτσι, η JavaScript της AcroForm, τα σενάρια επιπέδου εγγράφου και οι υπολογισμοί πεδίων εκτελούνται κάθε φορά που φορτώνεται το DLL με δυνατότητα V8. Ο οικοδεσπότης παραμένει υπεύθυνος μέσω ενός συνόλου συμβάντων που μπορούν να εμφανίσουν, να ανακατευθύνουν ή να αποκλείσουν οτιδήποτε προσπαθεί να κάνει το σενάριο

Αυτή η μία πρόταση επιλύει μια ερώτηση υποστήριξης που έχουμε δει σε διάφορες μορφές: "η φόρμα παραγγελίας μου υπολογίζει τα σύνολα γραμμών στο Acrobat αλλά όχι στην εφαρμογή μου", "το app.alert δεν εκτελείται ποτέ", "το πεδίο ημερομηνίας δεν μορφοποιείται μόνο του". Όλα αυτά ανάγονται στο ίδιο συμβόλαιο, και η κατανόησή του αξίζει δέκα λεπτά επειδή το ίδιο συμβόλαιο αποτελεί επίσης το όριο ασφαλείας σας έναντι εχθρικών εγγράφων

Γιατί οι υπολογισμοί φορμών PDF δεν λειτουργούν στο PDFium;

Το PDFium αντιμετωπίζει τη JavaScript εγγράφου ως αυστηρά προαιρετική (opt-in): εάν το μέλος m_pJsPlatform του περιβάλλοντος συμπλήρωσης φόρμας είναι NULL, κάθε σενάριο στο έγγραφο παρακάμπτεται σιωπηλά. Κανένα σφάλμα, καμία γραμμή καταγραφής, καμία μερική εκτέλεση. Τα υπολογιζόμενα πεδία διατηρούν την τελευταία αποθηκευμένη τιμή τους, το app.alert εξαφανίζεται, τα σενάρια μορφοποίησης δεν εκτελούνται ποτέ. Το Acrobat, το οποίο αποστέλλεται πάντα με τη δική του μηχανή JavaScript, εκτελεί με χαρά το ίδιο αρχείο, γι' αυτό και η ασυμφωνία μοιάζει τόσο πολύ με σφάλμα στον κώδικά σας, ενώ στην πραγματικότητα πρόκειται για μια εσκεμμένη προεπιλογή στην υποκείμενη βιβλιοθήκη

Το PDFium Component είχε ένα δεύτερο, ιστορικό επίπεδο σε αυτό. Έως την έκδοση 2.13.1 η σύνδεση επισύναπτε το m_pJsPlatform μόνο μέσα στον κλάδο προετοιμασίας XFA, οπότε τα απλά έγγραφα AcroForm, τα οποία αποτελούν τη συντριπτική πλειοψηφία των PDFs με σενάρια κώδικα, δεν έλαβαν ποτέ πλατφόρμα 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 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 εμφανίζουν τα αιτήματα ήχου (beep), εκτύπωσης, αλληλογραφίας και υποβολής φόρμας με τα πλήρη σύνολα παραμέτρων τους. Ένα πρόγραμμα προβολής που δεν εκχωρεί κανένα από αυτά εξακολουθεί να αποδίδει και να υπολογίζει σωστά· το έγγραφο απλά δεν μπορεί να ανοίξει διαλόγους, να εκτυπώσει ή να στείλει τίποτα

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 είναι τύπου ειδοποίησης (notification-style): Το PDFium Component δεν εκτελεί καμία ενέργεια δικτύου ή MAPI από μόνο του, οπότε ένα κακόβουλο έγγραφο που καλεί το this.submitForm ή το this.mailDoc έναντι ενός απροετοίμαστου οικοδεσπότη δεν επιτυγχάνει απολύτως τίποτα. Ο οικοδεσπότης πρέπει να συναινέσει εκχωρώντας έναν χειριστή και υλοποιώντας ο ίδιος τη μεταφορά, πράγμα που σημαίνει ότι η απόφαση να μετακινηθούν δεδομένα εκτός του μηχανήματος είναι πάντα δική σας, λαμβάνεται στον κώδικά σας, με το URL και το ωφέλιμο φορτίο στα χέρια σας

Πώς εμποδίζετε ένα κακόβουλο PDF από το να ανοίγει URLs;

Οι ενέργειες URI είναι το μόνο σημείο όπου η ιστορική προεπιλογή ήταν ενεργητική παρά παθητική, και γι' αυτό έλαβαν ένα ειδικό βέτο (veto). Πριν από την έκδοση 2.13.1 η επανάκληση FFI_DoURIActionWithKeyboardModifier εκτελούσε μέσω κελύφους (shell-execute) οποιοδήποτε URI παρείχε ένα πεδίο XFA, χωρίς όρους, γεγονός που έδινε σε ένα κακόβουλο έγγραφο τη δυνατότητα να ανοίγει αυθαίρετα URLs με ένα κλικ. Το 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) στο σχήμα είναι το ελάχιστο· ένα πιο αυστηρό πρόγραμμα προβολής επιβεβαιώνει με τον χρήστη ή δρομολογεί τα πάντα μέσω του δικού του απομονωμένου (sandboxed) στοιχείου προγράμματος περιήγησης. Η ίδια φιλοσοφία βέτο εκτείνεται σε όλο το σύνολο συμβάντων της 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;

Η ασυμμετρία επιμονής σε αυτό το σχόλιο είναι ένα πραγματικό όριο, όχι μια προειδοποίηση χάριν της φόρμας. Για τα πεδία κειμένου και σύνθετων πλαισίων (combo) της AcroForm, η επεξεργασμένη τιμή δεσμεύεται στην καταχώρηση /V του πεδίου κατά την απώλεια εστίασης και επιμένει μετά την αποθήκευση. Για τα πεδία κειμένου XFA, η εγγραφή καταλήγει μόνο στην προσωρινή μνήμη επεξεργασίας στη μνήμη, επειδή το PDFium δεν εκθέτει κανένα δημόσιο API για τη συμφιλίωση των τιμών widget πίσω στο πακέτο συνόλων δεδομένων XFA· η αποθήκευση του εγγράφου δεν θα μεταφέρει την αλλαγή. Εάν η απαίτηση είναι η ανθεκτική επεξεργασία δεδομένων XFA, επεξεργαστείτε το ίδιο το XML των συνόλων δεδομένων αντί για το widget

Όρια που αξίζει να γνωρίζετε πριν από την αποστολή

Δύο ειλικρινή όρια παραμένουν. Πρώτον, το JavaScript API που υλοποιεί το PDFium είναι ένα λειτουργικό υποσύνολο του μοντέλου αντικειμένων του Acrobat, επικεντρωμένο στον πυρήνα των σεναρίων φόρμας: πρόσβαση σε πεδία, συμβάντα υπολογισμού και μορφοποίησης, app.alert και app.response, αιτήματα εκτύπωσης, αλληλογραφίας και υποβολής. Έγγραφα που βασίζονται σε εξωτικά αντικείμενα που υπάρχουν μόνο στο Acrobat θα υποβαθμιστούν, συνήθως σιωπηλά, οπότε δοκιμάστε τις πραγματικές φόρμες που χειρίζονται οι χρήστες σας αντί να υποθέτετε ισοτιμία. Δεύτερον, η ενεργοποίηση του V8 σημαίνει αποστολή του pdfium.v8.dll, ενός σημαντικά μεγαλύτερου δυαδικού αρχείου από την απλή έκδοση. Ένα πρόγραμμα προβολής που δεν χρειάζεται ποτέ σενάρια κώδικα ή XFA μπορεί νόμιμα να διατηρήσει το μικρότερο DLL και να αφήσει το m_pJsPlatform μη συνδεδεμένο, κάτι που αποτελεί μια έγκυρη στάση ασφαλείας από μόνο του

Το μοτίβο που προκύπτει από όλα αυτά είναι ευχάριστα βαρετό: ορίστε το EnableV8Engine κατά την εκκίνηση, εκχωρήστε τα συμβάντα alert και response ώστε οι διάλογοι να φαίνονται εγγενείς, εκχωρήστε το OnXfaUriAction και καταγράψτε στο αρχείο ελέγχου τα συμβάντα mail και submit, και αφήστε όλα τα άλλα στην προεπιλεγμένη μηδενική λειτουργία (no-op) έως ότου μια απαίτηση ορίσει διαφορετικά. Η πλήρης αναφορά συμβάντων και η επιφάνεια API συμπλήρωσης φόρμας τεκμηριώνονται στη σελίδα προϊόντος του PDFium Component