Različice PDF Library for Delphi (PDFlibPas) pred v3.539.47 so lahko dekodirale escapirano besedilo dvakrat, ko so risale HTML ali Markdown v PDF. DrawHTMLText in DrawHTMLTextBox razčlenita HTML, ga normalizirata nazaj v HTML in razčlenita znova, tako da je besedilo, zapisano kot <unsafe>, pri drugi razčlenitvi prispelo kot prava oznaka. Od v3.539.47 je vsaka entiteta dekodirana točno enkrat, besedilo pa je povsod, kjer se spremeni nazaj v HTML, znova escapano
Scenarij, ki to izpostavi, je vsakdanji. Help desk izvozi tikete v PDF, komentar stranke pa gre v HTML predlogo. Razvijalec je naredil prav in escapiral komentar, tako da je <b> postal <b>. Znotraj izrisovalnika je bilo to escapiranje tiho razveljavljeno: komentar je izšel krepko, neznano ime oznake je preprosto izginilo s strani, escapirana sidrna povezava pa se je spremenila v klikabilno povezavo. Nobene izjeme, nobenega opozorila, popolnoma veljaven PDF, ki pove nekaj drugega kot podatki
Zakaj escapirano besedilo postane prava oznaka v PDF-ju?
Escapirano besedilo je postalo označba, ker izrisovalnik poganja dve razčlenitveni fazi, normalizacijski korak med njima pa je že dekodirano besedilo zapisal nazaj v HTML, brez da bi ga znova escapal. Vsaka dekodacija, ki jo je opravila prva razčlenitev, je bila potem drugi razčlenitvi na voljo kot živa sintaksa
Ti dve fazi obstajata z dobrim razlogom. Prva razčlenitev zgradi seznam elementov oznak in besed. NormalizeParsedHTML nato razreši kaskado slogovnih predlog: pravila iz blokov <style> primerja z vsako oznako, jih spoji z vrstičnimi atributi style, rezultat shrani na oznako in celoten seznam elementov sestavi nazaj v niz HTML. Faza postavitve razčleni ta normalizirani niz. To je isti mehanizem, ki poganja flexbox, CSS grid in postavitev opomb pod črto v HTML izrisovanju PDFlibPas
Napaka je bila v tem, kako so se besede sestavljale nazaj. Oznake so bile zapisane nazaj v izvirni obliki izvorne kode, besede pa v dekodirani obliki. Beseda, ki jo je prva razčlenitev dekodirala iz <unsafe> v <unsafe>, je pristala v normaliziranem HTML kot surova kotna oklepaja, druga razčlenitev pa ju je prebrala kot element. Okoli tega osrednjega hrošča so ležila tri manjša ohlapna mesta, ki so kazala isto smer:
&ni bil v podprtem naboru entitet, zato se jeR&Dnatisnil dobesedno in ni bilo načina, da bi zapisali dobesedni zapis entitete, kot je<, kot besedilo- Risalna faza je
zamenjala še drugič, potem ko je bilo razčlenjanje že končano, tako da je lahko dobesedni zapis entitete izginil še na samem koncu - Escapanje kode v Markdown je preskočilo ampersand, izvoznik podatkovnih zbirk pa je escapal samo kotne oklepaje, tako da so se zapisi entitet znotraj kode ali vrednosti celic dekodirali kot označba
| Vnos, ki doseže izrisovalnik | Pred v3.539.47 | Od v3.539.47 |
|---|---|---|
<unsafe> | Razčlenjeno kot oznaka, besedilo ne doseže strani | <unsafe> narisano kot besedilo |
<b>x</b> | x narisano krepko | <b>x</b> narisano kot besedilo |
R&D | R&D natisnjeno dobesedno | R&D |
&lt; | &lt; natisnjeno dobesedno | < |
Kodni razpon Markdown z | Postal je nedeljivi presledek | narisano kot besedilo |
Vrednost celice podatkovne zbirke < | < | < |
Kako v3.539.47 naredi dekodiranje HTML entitet enofazno
PDFlibPas v3.539.47 naredi dekodiranje entitet enofazno s tremi usklajenimi spremembami: razčlenjevalnik dekodira & zadnjega, risalna faza ne dekodira ničesar več, vsako mesto, ki pretvarja dekodirane besede nazaj v HTML, pa jih najprej znova escapa
Podprti nabor entitet za besedilno vsebino je zdaj <, >, & in . Vse drugo, vključno s številskimi sklici, kot je A, in imenskimi entitetami, kot je ", ostane dobesedno besedilo. Ta meja je pomembna za to, kako escapate svoj lasten vnos, kot je pokazano spodaj
Vrstni red znotraj dekodirnika je prvi popravek. Če bi & dekodirali prvega, bi se vhod &lt; spremenil v <, naslednja zamenjava pa bi ga pretvorila v < — dvojna dekodacija, ki se zgodi znotraj ene same faze. Pot ANSI besed zato najprej zamenja <, > in , & pa zadnjega, tako da ampersand, ki ga izdela, ni nikoli več pregledan. Pot UTF-16 besed je en sam pregled od leve proti desni v dvobajtnih korakih, ki vsak zadetek prepše na mestu in gre čez njega, kar strukturno daje enako jamstvo
Drugi popravek odstrani pozno zamenjavo iz risalne faze. Dekodiranje pripada razčlenjevalniku in nikamor drugam, tako da je beseda, ki doseže lomilnik vrstic, končno besedilo
Tretji popravek je pravilo meje. NormalizeParsedHTML zdaj escapa &, < in > v vsaki dekodirani besedi, preden jo doda normaliziranemu HTML. Druga razčlenitev ga dekodira nazaj v točno isto besedilo, tako da je skupni učinek celega cevovoda ena sama dekodacija. Nadaljevalni niz sledi istemu pravilu: besede, ki se niso prilegale v okvir, se escapajo, preden se dodajo LeftOverText, preostanek ostanka pa se kopira iz normaliziranega HTML, ki je že v escapirani obliki. Zanka, ki nabira tiste preostale besede, je zdaj omejena tudi s številom besed, medtem ko se je stara ponavljajoča zanka lahko podala čez zadnjo besedo
Zakaj escapiranje UTF-16BE ne more uporabiti zamenjave na ravni bajtov?
Escapanje UTF-16BE ne more uporabiti zamenjave na ravni bajtov, ker se lahko dvobajtni vzorec za ampersand razpne čez dva nepovezana znaka. Edina pravilna enota dela je celotna 16-bitna kodna enota
Izrisovalnik shranjuje Unicode besede kot big-endian UTF-16, spravljene v bajtne nize, najprej visoki bajt. Ampersand je 00 26. Vzemimo zdaj U+0100 (latinska velika A z makronom, bajti 01 00), ki ji sledi U+2603 (sneženi možek, bajti 26 03). Bajtno zaporedje je 01 00 26 03, bajta dva in tri pa bereta 00 26. Bajtno iskanje po #0'&' najde ampersand, ki ne obstaja, vstavi bajte za & na sredino dveh znakov in vsak naslednji znak odreže za en bajt
To ni eksotični robni primer. Vsak znak, katerega nizki bajt je nič, lahko prispeva prvo polovico; U+4E00, eden najpogostejših CJK ideogramov, se kvalificira. Kotni oklepaji imajo enako izpostavljenost: 00 3C in 00 3E se pojavita vsakič, kadar takemu znaku sledi eden iz območja U+3C00 do U+3EFF v CJK Extension A. Popravek v EscapeHTMLWord razpaka bajte v WideString, escapa znak po znaku in rezultat znova spravi skupaj. Dekodirna stran je bila že varna, ker preizkuša vzorce samo na sodih mejah kodnih enot
Isto pravilo velja za vašo lastno kodo. Če besedilo UTF-16 kjer koli hranite kot TBytes, na primer po TEncoding.BigEndianUnicode.GetBytes, ga ne iščite po bajtnih vzorcih. Pretvorite nazaj v niz in delajte na znakih
Kodni bloki Markdown in izvozi podatkovnih zbirk: ampersand escapajte najprej
Od v3.539.47 oba izdelovalca HTML znotraj PDFlibPas, pretvornik Markdown in izvoznik podatkovnih zbirk, escapata ampersand pred kotnimi oklepaji, tako da enojna dekodacija v izrisovalniku povrne točno izvirno besedilo
V MarkdownToHTML vrstični kodni razponi in ograjeni ali zamaknjeni kodni bloki zdaj preslikajo & v &, < v < in > v >, presledki pa postanejo , tabulator pa štirje takšni, da ostane zamik. Običajna proza v Markdown escapa samo kotne oklepaje, tako da surovi HTML v prozi ne more vbrizgati oznak, avtor pa lahko še vedno namenoma napiše &, kar avtorji Markdown pričakujejo. DrawMarkdownText in DrawMarkdownTextBox uporabljata isto pretvorbo, zato se koda v PDF-ju pokaže točno tako, kot je natipkana:
uses
System.SysUtils, PDFlibrary;
procedure RenderCodeSample;
var
Lib: TPDFlib;
Md, Html: WideString;
begin
Md := 'Comparison helper:' + sLineBreak + sLineBreak +
'```' + sLineBreak +
'if (A < B) and (Flags <> 0) then' + sLineBreak +
' WriteLn(''<tag> & R&D'');' + sLineBreak +
'```';
Lib := TPDFlib.Create;
try
// Preglejte HTML: v kodi se '&' spremeni v '&' in '<' v '<'
Html := Lib.MarkdownToHTML(Md);
Lib.SetOrigin(1); // izhodišče zgoraj levo, Y raste navzdol
Lib.SetMeasurementUnits(0); // točke
// Stran pokaže kodo točno tako, kot je bila natipkana, z zapisi entitet
Lib.DrawMarkdownText(50, 50, 495, Md);
Lib.SaveToFile('code-sample.pdf');
finally
Lib.Free;
end;
end;
Izvoznik podatkovnih zbirk je poučen primer. Pred v3.539.47 je escapal samo kotne oklepaje, in to namenoma: izrisovalnik ni dekodiral &, tako da bi escapiranje ampersanda natisnilo & v vsaki celici, ki ga vsebuje. Obvozna pot je bila pravilna za stari izrisovalnik in splošno napačna, ker je bila vrednost celice, ki je po naključju vsebovala <, dekodirana v <. S popravljenim izrisovalnikom izvoznik escapa & najprej, vrednost, kot je R&D < & , pa pristane v PDF dobesedno. Če gradite poročila na ta način, vam vodnik po izvozu TDataSet v PDF poročilo v Delphiju pokrije preostanek izvoznika
Zakaj mora ampersand na prvo mesto, je vredno enkrat razložiti. Escapajte < najprej in dobite <; escapajte & drugič in to postane &lt;, kar pravilna enojna dekodacija prikaže kot < namesto <. Veriga zaporednih zamenjav je pravilna samo, kadar je ubežni znak sam obravnavan, preden karkoli, kar ga vnaša
Kako naj escapate nezaupljivo besedilo za DrawHTMLTextBox?
Za HTML izrisovanje v PDFlibPas escapajte nezaupljivo besedilno vsebino z zamenjavo &, nato <, nato >, točno enkrat, nezaupljive podatke pa povsem držite izven vrednosti atributov
uses
System.SysUtils, PDFlibrary;
// Escapa nezaupljivo besedilo za HTML besedilno vsebino v PDFlibPas.
// '&' je treba zamenjati najprej, sicer bi ampersand znotraj
// že izdelanega '<' bil escapiran še drugič
function EscapeHTMLText(const S: string): string;
begin
Result := StringReplace(S, '&', '&', [rfReplaceAll]);
Result := StringReplace(Result, '<', '<', [rfReplaceAll]);
Result := StringReplace(Result, '>', '>', [rfReplaceAll]);
end;
procedure RenderTicket(const CustomerComment: string);
var
Lib: TPDFlib;
Html: WideString;
begin
Lib := TPDFlib.Create;
try
Lib.SetOrigin(1);
Lib.SetMeasurementUnits(0);
Html := '<p><b>Customer comment</b></p>' +
'<p>' + EscapeHTMLText(CustomerComment) + '</p>';
Lib.DrawHTMLText(50, 50, 495, Html);
Lib.SaveToFile('ticket.pdf');
finally
Lib.Free;
end;
end;
Na v3.539.47 se komentar, kot je Try <a href="https://example.com">this</a> & <b>, pokaže na strani znak za znakom. Pred v3.539.47 je lahko enak escapiran vhod izdelal živo povezavo, kar je tisti del, ki iz zaslonske napake naredi varnostni problem: komentar tiketa nikoli ne bi smel zneti klikabilni URL v dokument, ki mu vaše osebje zaupa
Opazite, česa funkcija ne escapa. Splošni escapirniki HTML pretvorijo tudi " v " in ' v ', kar je prav za brskalnik. Dekodiranje besedila v PDFlibPas prepozna samo štiri prej naštete entitete, zato bi se ta dva natisnila dobesedno kot " in '. Navedki so v besedilni vsebini neškodljivi; štejejo samo znotraj vrednosti atributov, izrisovalnik pa entitet v atributih sploh ne dekodira. Varen načrt je zato ne boljši escapirnik, ampak pravilo: nezaupljivi podatki nikoli ne gredo v href, src ali style. Če cilj povezave res mora priti iz uporabniških podatkov, ga preverite sami proti belemu seznamu shem in znakov ter zavrzite vse, kar vsebuje navedke ali kotne oklepaje
Iz popravka neposredno sledi dve opombi ob nadgradji:
- Če je vaša koda prenehala escapati
&, ker so starejše različice&natisnile dobesedno, jo dodajte nazaj. Brez nje se uporabniško besedilo, ki vsebuje<, zdaj prikaže kot<— še vedno neškodljivo besedilo, a ne več to, kar je uporabnik natipkal - Ne escapajte dvakrat. Besedilo, ki gre skozi dva escapirnika, izriše
<kot vidni zapis<, zato poiščite tisto eno mejo, kjer vaši podatki vstopijo v HTML, in escapajte samo tam
Paginacija z LeftOverText brez razbijanja escapanj
DrawHTMLTextBox vrne HTML, ki se ni prilegal, običajno imenovan LeftOverText, od v3.539.47 pa ta ostanek ohrani dobesedne zapise entitet in escapirane kotne oklepaje, ko ga podate naslednjemu okviru. Pravilo za klicalce je preprosto: podajte ga nazaj nespremenjen
const
BoxLeft = 50;
BoxTop = 50;
BoxWidth = 495; // dimenzionirano za stran A4 v točkah
BoxHeight = 740;
MaxPages = 500;
procedure RenderLongHTML(Lib: TPDFlib; const Html: WideString);
var
Rest: WideString;
Pages: Integer;
begin
Lib.SetOrigin(1);
Lib.SetMeasurementUnits(0);
Rest := Lib.DrawHTMLTextBox(BoxLeft, BoxTop, BoxWidth, BoxHeight, Html);
Pages := 1;
while (Rest <> '') and (Pages < MaxPages) do
begin
Lib.NewPage;
Inc(Pages);
// LeftOverText je že escapran HTML motorja: nikoli ga ne escapajte ali razescapajte
Rest := Lib.DrawHTMLTextBox(BoxLeft, BoxTop, BoxWidth, BoxHeight, Rest);
end;
if Rest <> '' then
raise Exception.CreateFmt('Content still left after %d pages', [MaxPages]);
end;
Ostanek obravnavajte kot neprozorno vsebino. To je normalizirani HTML motorja, s slogi, ki so že razrešeni, zato ga ne podajajte skozi svoj escapirnik, ga ne dekodirajte in ne vpletajte vanj uporabniškega besedila. Omejitev strani je poceni zavarovanje: če se kakšen element nikoli ne more spraviti v okvir, neomejena zanka nima naravnega izhoda
Markdown ima svoje nadaljevanje. DrawMarkdownTextBox vrne žeton, ki se začne z notranjo označbo, da lahko naslednji klic preskoči pretvorbo; vrnite ga DrawMarkdownTextBox ali DrawMarkdownText, ne pa vstopnim točkam HTML, ki bi označbo narisale kot besedilo
Splošna lekcija: dekodiraj enkrat, ponovno kodiraj na vsaki meji
Vsak cevovod, ki razčlenjuje besedilo, rezultat sestavi nazaj v isto sintakso in ga razčleni znova, mora dekodiranje obravnavati kot operacijo, ki se zgodi točno na enem mestu, in mora znova kodirati na vsaki meji, kjer dekodirano besedilo spet postane sintaksa. Motorji predlog, očistniki HTML in verige Markdown-v-HTML-v-PDF imajo isto obliko in odpovedo se enako, ko sestavljalec pozabi, da izdeluje označbo
Simptomi so napovedljivi, ko poznate obliko. Premalo ponovnega kodiranja spremeni podatke v sintakso, kar je smer vbrizgavanja. Preveč kodiranja ali dekodirnik, ki steče dvakrat, pokaže bralcu zapise entitet ali pa jih poje, kar je smer prikaza. Popravljanje samo ene smeri običajno pokvari drugo, zato je moral popravek PDFlibPas v isti izdaji dodati dekodiranje &, spremeniti njegov vrstni red, odstraniti pozno dekodacijo in dodati ponovno escapiranje. Isti načel teče v obratni smeri, ko se vsebina PDF izvozi kot strukturirano besedilo, kot pri semantičnem izvozu PDF v Markdown in DOCX iz Delphija, kjer se mora vsak dobesedni znak escapati za ciljno sintakso točno enkrat
Pregledni seznam za hitri pregled
- Nadgradite na PDFlibPas v3.539.47 ali novejši, če izrisujete HTML ali Markdown, ki vsebuje uporabniške podatke
- Escapajte besedilno vsebino z
&najprej, nato<in>; navedkov za besedilo PDFlibPas ne pretvarjajte - Escapajte enkrat, na enem samem mestu, kjer podatki vstopijo v niz HTML
- Nezaupljive vrednosti držite izven
href,srcinstyle, ali pa jih preverite proti belemu seznamu - Pričakujte, da se v besedilu dekodirajo samo
<,>,&in ; druge entitete ostanejo dobesedne LeftOverTextpodajte nazajDrawHTMLTextBoxnespremenjen in zanki strani postavite omejitev- Žetone nadaljevanja Markdown podajte samo
DrawMarkdownTextBoxaliDrawMarkdownText - Nikoli ne iščite bajtnih vzorcev v UTF-16 bajtnih predpomnilnikih; delajte na celih kodnih enotah
HTML in Markdown izrisovanje, izvoz poročil podatkovnih zbirk in preostanek motorja postavitve prihaja v izvorni kodi Pascal PDF Library for Delphi, za Delphi in Free Pascal. Poglejte stran izdelka PDFlibPas za izdaje, podporo platformam in preskusni prenos