Techninis straipsnis

PDF priėmimo peržiūros darbo aplinkos kūrimas Delphi su PDFium

PDF priėmimo peržiūros darbo aplinka (intake review workbench) yra nedidelė programa, atliekanti vieną užduotį: patikrinti kiekvieną failą prieš leidžiant bet kokioms vėlesnėms sistemoms jį apdoroti. Kad atliktų šį darbą, ji turi apjungti keletą galimybių į vieną etapą. Ji atidaro failą (nepasitikėdama juo), nuskaito, ką failas praneša apie save, ieško turinio, kuris galėtų suklaidinti paprastą teksto išgaviklį arba sukelti atsaką, nusprendžia, ar išvis yra išgaunamo teksto, ir nukreipia dokumentą į atitinkamą eilę pagal tai, ką rado. Praleiskite šį patikrinimą, ir gedimai bus tylūs: savininko slaptažodžiu užšifruotas PDF failas, kuriame apgaubta XFA forma, teksto išgaviklyje bus nuskaitytas kaip tuščios eilutės, bus suindeksuotas kaip tuščias dokumentas ir niekas to nepastebės, kol kas nors vėlesnėje sistemoje nepradės ieškoti turinio, kuris taip ir nebuvo perskaitytas. „PDFium Component“ yra VCL/LCL peržiūros ir tikrinimo biblioteka su šaltinio kodu, skirta Delphi, C++Builder ir Lazarus, ir ji pateikia introspekcijos iškvietimus, reikalingus šiai darbo aplinkai. Toliau esančiuose skyriuose aptariama, kuris iškvietimas atsako į kurį klausimą, ir dvi vietos, kur akivaizdus iškvietimas pateikia visiškai klaidingą atsakymą

Penki klausimai, į kuriuos reikia atsakyti prieš nukreipiant failą

Atsisakius tinklelio ir miniatiūrų juostos, gautų failų rūšiavimas susitraukia iki penkių klausimų:

Delphi PDF priėmimo darbo aplinkos diagrama, atsakanti į penkis rikiavimo klausimus vienu pigiu atvėrimu ir nukreipianti failus į paruošta, peržiūrėti, užblokuota arba sugadinta būsenas
Priėmimo triažas atsako į penkis klausimus vienu pigiu atvėrimu ir nukreipia failą į paruošta, peržiūrėti, užblokuota arba sugadinta
  • Ar failą išvis galima atidaryti ir su kokiu slaptažodžiu?
  • Kuo jis teigia esąs: pavadinimas, autorius, sukūrimo data?
  • Ar jame yra aktyvaus ar rizikingo turinio, pavyzdžiui, JavaScript, XFA forma arba įterptieji failai?
  • Ar yra išgaunamo teksto, ar tai tik skenuotas vaizdas, skirtas OCR?
  • Atsižvelgiant į visa tai, į kurią eilę jis patenka: tiesioginio apdorojimo, rankinės peržiūros ar karantino?

Kiekvienas klausimas susiejamas su vienu arba dviem „PDFium Component“ iškvietimais. Du iš šių susiejimų turi aštrių kampų, kurie lemia didžiąją dalį neteisingai nukreiptų failų gamybinėse sistemose. Dokumento metaduomenys gyvena dviejose skirtingose vietose, kurios gali nesutapti, o šifravimas nebūtinai trukdo atidaryti dokumentą

Pigus atidarymas: išjungtas formų pildymas, neatvaizduojami puslapiai

Rūšiavimas turėtų būti kuo pigesnis atidarymo procesas. Nustačius FormFill := False prieš Active := True, komponentui nurodoma visiškai praleisti formų pildymo aplinką. Tai sutrumpina įkėlimo laiką ir (tai ne mažiau svarbu failams iš nežinomų šaltinių) neleidžia inicijuoti dokumento lygio JavaScript. Nė viena iš toliau naudojamų tikrinimo savybių nereikalauja puslapio atvaizdavimo, todėl rūšiavimo etape nereikia sukurti nė vieno taškinio paveikslėlio

procedure InspectIncoming(const IncomingPath: string; var Rec: TIntakeRecord);
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := IncomingPath;
    Pdf.FormFill := False;     // jokios formų aplinkos, jokio JavaScript inicijavimo
    Pdf.Active := True;        // nesėkmė yra tyli: Active tiesiog lieka False

    if not Pdf.Active then
    begin
      Rec.OpenFailed := True;  // sugadintas failas arba vartotojo slaptažodžio užraktas
      Exit;                    // finally blokas vis tiek įvyksta
    end;

    Rec.PageCount := Pdf.PageCount;
    CollectIdentity(Pdf, IncomingPath, Rec);
    CollectRiskSignals(Pdf, Rec);
  finally
    Pdf.Active := False;
    Pdf.Free;                  // niekada nenutekinkite egzemplioriaus prie netaisyklingo failo
  end;
end;

Patikrinimas po priskyrimo yra privalomas, ir tai yra patikrinimas, o ne išimties doroklis dėl tam tikros priežasties. Kai variklis negali įkelti failo, komponentas nutolina vidinę EPdfError klaidą ir palieka Active reikšmę False, užuot ją propagavęs. Kodas, kuris laukia išimties, sėkmingai perskaitys PageCount iš dokumento, kuris taip ir nebuvo atidarytas. Jei atmetimo darbo eigai reikia tikrojo variklio klaidos teksto, nuskaitykite failą į baitų masyvą ir iškvieskite LoadDocument perkrovą, kuri priima TBytes; šis kelias sukelia EPdfError su klaidos pranešimu, įskaitant slaptažodžio atvejį. Blokas try..finally vis tiek išlaiko savo vertę. Priėmimo paslaugos veikia be priežiūros savaitėmis, todėl jokia vėlesnė išimtis neturi nutekinti TPdf egzemplioriaus arba išlaikyti failo užrakto, ant kurio suklups pakartotinis bandymas

Pralaidumas retai tampa kliūtimi. Kai formų pildymas išjungtas ir neatliekamas atvaizdavimas, rūšiavimo atidaryme dominuoja įvestis / išvestis (I/O), o vienas darbininkas patogiai patikrina kelis failus per sekundę iš vietinio disko. Jei gautų failų kiekis kada nors viršija vieno darbininko pajėgumą, padalykite darbą pagal failus, o ne pagal patikrinimus. Visi penki klausimai dalijasi vienu atidarymu, o jų padalijimas per kelis procesus padaugintų brangiausią žingsnį, užuot jį amortizavęs

Metaduomenys gyvena dviejose vietose, ir jos nesutampa

ISO 32000-1 apibrėžia dvi dokumento metaduomenų buveines: dokumento informacijos žodyną (14.3.3 punktas) ir XMP paketą, prisegtą prie katalogo (14.3.2 punktas). Savybės Title, Author, Subject ir CreationDate nuskaito Info žodyną, su MetaText[] bet kokiam kitam raktui ir DecodeDate, kad išanalizuotų D:YYYYMMDD... datos eilutę. Tačiau problema yra ta, kad šiuolaikiniai kūrėjai vis dažniau rašo tik XMP – šią kryptį ISO 32000-2 oficialiai patvirtina atsisakydama daugumos Info žodyno raktų PDF 2.0 versijoje. Simptomas priėmimo įrankyje yra konkretus. Jūsų darbo aplinka rodo tuščią pavadinimą, o „Adobe Acrobat“ jį rodo, nes „Acrobat“ grįžta prie dc:title XMP paketo viduje, kurio Info žodyno savybės niekada neliečia

PDF metaduomenų, gyvenančių dviejose vietose — Info žodyne ir XMP pakete, kurie gali nesutarti dėl pavadinimo Delphi priėmimo įrankyje, diagrama
Dokumento metaduomenys gyvena Info žodyne ir XMP pakete, o du namai gali nesutarti dėl pavadinimo
procedure CollectIdentity(Pdf: TPdf; const FilePath: string;
  var Rec: TIntakeRecord);
begin
  Rec.Title := Pdf.Title;             // Info žodyno reikšmė
  Rec.Author := Pdf.Author;
  Rec.CreatedAt := Pdf.CreationDate;  // neapdorota PDF datos eilutė ("D:2026...")

  // Tuščias Info pavadinimas nereiškia, kad dokumentas neturi pavadinimo. Komponentas
  // neatskleidžia XMP paketo, todėl patikrinkite neapdorotus failo
  // baitus dėl dc:title elemento prieš patikint tuštuma.
  if (Rec.Title = '') and FileContainsText(FilePath, 'dc:title') then
    Include(Rec.Flags, ifTitleInXmpOnly);
end;

Net ir paprastas aukščiau parodytas eilutės patikrinimas yra naudingas: faktas, kad „metaduomenys yra, bet ne ten, kur ieško senosios programos“, yra svarbus bet kokiam archyvavimo konvejeriui, kuris indeksuoja pagal pavadinimą arba autorių. Jei jūsų tolimesnis indeksas nuskaito tik Info žodyną, taip pažymėti failai tyliai taps neieškomi

Užšifruoti failai, kurie vis tiek atsidaro

Užšifruoto dokumento atidarymas nebūtinai nepavyksta. Standartinis saugumo doroklis (ISO 32000-1, 7.6.3 punktas) skiria vartotojo slaptažodį (user password), reikalingą dokumentui atidaryti, nuo savininko slaptažodžio (owner password), kuris tiesiog nustato leidimus, tokius kaip spausdinimas ir kopijavimas. Didelė dalis „apsaugotų“ verslo dokumentų yra užšifruoti naudojant savininko slaptažodį ir tuščią vartotojo slaptažodį. Jie atsidaro be jokių raginimų, visiškai išsišifruoja ir pasikliauja tuo, kad peržiūros programos savanoriškai laikysis leidimų vėliavėlių. Tai yra politika, o ne tikra apsauga, ir jūsų priėmimo būsenos turėtų atspindėti šį skirtumą

Šifravimo aptikimui po sėkmingo atidarymo reikia vieno variklio iškvietimo ir atsarginio varianto. FPDF_GetSecurityHandlerRevision(Pdf.Document) grąžina -1 neapsaugotiems failams ir doroklio versiją kitais atvejais, o Pdf.Permissions, grąžinantis bet ką kitą nei visų nustatytų bitų kaukė $FFFFFFFF, yra patvirtinantis signalas. Tikriems vartotojo slaptažodžiu apsaugotiems failams priskirkite Password prieš nustatydami Active := True; jei atidarymas vis tiek nepavyksta, nukreipkite failą į blokuojamą būseną, kurioje prašoma kredencialų iš siuntėjo saugiu kanalu, užuot aklai bandžius iš naujo. Taip pat venkite pagundos automatiškai karantinuoti visus „užšifruotus“ failus. Daugelyje dokumentais paremtų pramonės šakų užšifruoti, bet atidaromi failai yra įprastas, o ne įtartinas atvejis

Aktyvus turinys: JavaScript, XFA ir įterptieji failai

Trys radiniai visada turėtų daryti įtaką nukreipimo sprendimui. Pirma, JavaScript: įvykis OnUnsupportedFeature praneša apie struktūrines funkcijas, tokias kaip XFA arba 3D turinys, kai variklis su jomis susiduria, tačiau jis neaptinka JavaScript. Vietoj to patikrinkite JavaScriptActionCount ir traktuokite nelygų nuliui rezultatą kaip aktyvų turinį. Antra, XFA: kai FormType grąžina ftXfaFull, matomi puslapiai dažnai yra tik XFA šablono atvaizdavimas, o įprastas teksto išgavimas matys šablono tekstą, o ne užpildytas reikšmes. Trečia, priedai: PDF yra konteinerio formatas, ir AttachmentCount praneša, ar šis failas turi įterptų priedų

PDF priėmimo rizikos signalų Delphi diagrama: šifravimo doroklio revizija, JavaScript veiksmų skaičiai, XFA formos tipas ir pavojingi priedai
Šifravimo būsena ir JavaScript, XFA bei priedų skaičiai yra signalai, kurie privalo išgyventi iki maršrutizavimo sprendimo
procedure CollectRiskSignals(Pdf: TPdf; var Rec: TIntakeRecord);
var
  i, PageNo: Integer;
  Ext: string;
begin
  Rec.IsEncrypted := Assigned(FPDF_GetSecurityHandlerRevision) and
    (FPDF_GetSecurityHandlerRevision(Pdf.Document) <> -1);
  Rec.HasForms := Pdf.FormType <> ftNone;
  Rec.IsXfa := Pdf.FormType = ftXfaFull;
  Rec.HasJavaScript := Pdf.JavaScriptActionCount > 0;

  // AnnotationCount yra vieno puslapio savybė; eikite per puslapius, kad susumuotumėte
  // ją. Puslapio objekto įkėlimas nieko neatvaizduoja, todėl tai lieka pigu.
  Rec.Annotations := 0;
  for PageNo := 1 to Pdf.PageCount do
  begin
    Pdf.PageNumber := PageNo;
    Inc(Rec.Annotations, Pdf.AnnotationCount);
  end;

  Rec.Attachments := Pdf.AttachmentCount;

  for i := 0 to Rec.Attachments - 1 do
  begin
    Ext := LowerCase(ExtractFileExt(string(Pdf.AttachmentName[i])));
    if (Ext = '.exe') or (Ext = '.js') or (Ext = '.vbs') or (Ext = '.dll') then
      Include(Rec.Flags, ifDangerousAttachment);
  end;
end;

Dvi šio ciklo detalės reikalauja dėmesio. Priedo pavadinimas gaunamas iš dokumento vidaus, todėl niekada nenaudokite jo kaip išvesties kelio be išankstinio sanitarizavimo; įterptas pavadinimas, toks kaip ..\..\start.exe, yra pasiruošęs kelio kirtimo (path traversal) incidentas neatsargaus išsaugojimo metu. Be to, plėtinių blokavimo sąrašas yra tik įspėjimas, o ne garantija. Jo darbas yra priversti priimti žmogaus sprendimą, o ne patvirtinti, kad failas yra švarus

Signalų pavertimas nukreipimo būsenomis

Veikiančiam būsenos modeliui reikia mažiau būsenų, nei tikisi dauguma komandų: ready (nėra kliūčių, yra tekstas), review (atidaryti pavyko, bet reikia peržiūrėti, pavyzdžiui, yra XFA forma, JavaScript, tuščias teksto sluoksnis arba pavadinimas tik XMP), blocked (reikalingas vartotojo slaptažodis) ir damaged (nepavyko atidaryti). Įrašykite įrodymus kartu su būsena. Failo maišos kodas (hash), puslapių skaičius, tikslios vėliavėlės ir variklio klaidos pranešimas sugadintiems failams – visa tai yra svarbu, nes asmuo, kuris suabejos nukreipimo sprendimu, padarys tai po kelių savaičių su failu, kuris tuo metu jau gali būti pakeistas arba modifikuotas

Kai operatoriui tikrai reikia peržiūrėti karantinuotą failą, neperduokite jo numatytajai sistemos peržiūros programai. Atvaizduokite jį apsaugotame skydelyje su išjungtais scenarijais ir nuorodų valdymu – šis metodas aprašytas straipsnyje apie saugaus PDF peržiūros paviršiaus kūrimą Delphi. Ir jei jūsų sistema teikia duomenis archyvui su atitikties reikalavimais, rūšiavimo etapas yra natūrali vieta planuoti gilesnį patikrinimą; paketinis patikrinimas (preflight) pagal PDF/A ir PDF/UA profilius tęsia darbą būtent ten, kur baigiasi šis patikrinimas

Komponento produkto puslapyje aprašomas licencijavimas, visa patikrinimo API ir pridėtos demonstracinės programos, įskaitant dokumentų inspektorių: „PDFium Component