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ų:
- 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; // no form environment, no JavaScript init
Pdf.Active := True; // failure is silent: Active simply stays False
if not Pdf.Active then
begin
Rec.OpenFailed := True; // damaged file or user-password lock
Exit; // the finally block still runs
end;
Rec.PageCount := Pdf.PageCount;
CollectIdentity(Pdf, IncomingPath, Rec);
CollectRiskSignals(Pdf, Rec);
finally
Pdf.Active := False;
Pdf.Free; // never leak the instance on a malformed file
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.
procedure CollectIdentity(Pdf: TPdf; const FilePath: string;
var Rec: TIntakeRecord);
begin
Rec.Title := Pdf.Title; // Info dictionary value
Rec.Author := Pdf.Author;
Rec.CreatedAt := Pdf.CreationDate; // raw PDF date string ("D:2026...")
// An empty Info title does not mean the document is untitled. The
// component does not expose the XMP packet, so probe the raw file
// bytes for the dc:title element before trusting the blank.
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ų.
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 is a per-page property; walk the pages to total
// it. Loading a page object renders nothing, so this stays cheap.
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“.