PDF Library for Delphi (PDFlibPas) -versiot ennen v3.539.47:ää saattoivat dekoodata escapetun tekstin kahdesti piirtäessään HTML:ää tai Markdownia PDF:ään. DrawHTMLText ja DrawHTMLTextBox jäsentävät HTML:n, normalisoivat sen takaisin HTML:ksi ja jäsentävät sen sitten uudelleen, joten teksti, joka oli kirjoitettu muodossa <unsafe>, saapui toiseen jäsennykseen aitona tagina. Versiosta v3.539.47 alkaen jokainen entiteetti dekoodataan täsmälleen kerran, ja teksti escapataan uudelleen jokaisessa paikassa, jossa se muuttuu takaisin HTML:ksi
Tilanne, joka paljastaa vian, on tavallinen. Help desk vie tikettejä PDF:ään, ja asiakkaan kommentti menee HTML-mallipohjaan. Kehittäjä teki oikein ja escapasi kommentin, joten <b> muuttui muodoksi <b>. Rendererin sisällä escapaus kumottiin hiljaisesti: kommenti tuli ulos lihavoituna, tuntematon taginimi katosi sivulta ja escapattu ankkuri muuttui napsautettavaksi linkkiannotaatioksi. Ei poikkeusta, ei varoitusta, täysin kelvollinen PDF, joka sanoo jotain muuta kuin data
Miksi escapattu teksti muuttuu oikeaksi tagiksi PDF:ssä?
Escapattu teksti muuttui merkinnäksi, koska rendereri ajaa kaksi jäsennysvaihetta, ja niiden välinen normalisointivaihe kirjoitti jo dekoodatun tekstin takaisin HTML:ksi escapamatta sitä uudelleen. Jokainen ensimmäisen jäsennyksen suorittama dekoodaus oli silloin toisen jäsennyksen käytettävissä elävänä syntaksina
Kaksi vaihetta on olemassa hyvän syyn takia. Ensimmäinen jäsennys rakentaa luettelon tagi- ja san-elementeistä. NormalizeParsedHTML ratkaisee sitten tyylisivukaskadin: se sovittaa <style>-lohkojen säännöt jokaista tagia vasten, yhdistää ne inline-style-attribuutteihin, tallettaa tuloksen tagiin ja serialisoi koko elementtiluettelon takaisin HTML-merkkijonoksi. Asetteluvaihe jäsentää kyseisen normalisoidun merkkijonon. Kyseinen koneisto pyörittää myös artikkelin flexbox, CSS grid ja alaviiteasettelu PDFlibPasin HTML-renderöinnissä kuvaamaa toimintoa
Vika oli siinä, miten sanat serialisoitiin. Tagit kirjoitettiin takaisin niiden alkuperäisessä lähdemuodossa, kun taas sanat kirjoitettiin takaisin dekoodattussa muodossaan. Sana, jonka ensimmäinen jäsennys oli dekoodannut muodosta <unsafe> muotoon <unsafe>, päätyi normalisoituun HTML:ään raakoina kulmasulkeina, ja toinen jäsennys luki sen elementtinä. Tämän ydinbugin ympärillä kyti kolme pienempää vuotoa, jotka osoittivat samaan suuntaan:
&ei kuulunut tuettujen entiteettien joukkoon, jotenR&Dtulostui kirjaimellisena eikä ollut mitään tapaa kirjoittaa kirjaimellista entiteettimuotoa kuten<tekstinä- Piirtovaihe korvasi
in toistamiseen vasta jäsennyksen jo päätyttyä, joten kirjaimellinen entiteettimuoto saattoi vielä kadota aivan lopussa - Markdownin koodiescapaus ohitti et-merkin ja dataset-viejä escapasi vain kulmasulkeet, joten entiteettimuodot koodin sisällä tai soluarvoissa dekoodattiin merkinnäksi
| Rendereriin saapuva syöte | Ennen v3.539.47 | Versiosta v3.539.47 alkaen |
|---|---|---|
<unsafe> | Jäsennettiin tagina, teksti ei koskaan päädy sivulle | <unsafe> piirretään tekstinä |
<b>x</b> | x piirrettiin lihavoituna | <b>x</b> piirretään tekstinä |
R&D | R&D tulostui kirjaimellisena | R&D |
&lt; | &lt; tulostui kirjaimellisena | < |
Markdownin koodialue, joka sisältää | Muuttui kiinteäksi välilyönniksi | piirretään tekstinä |
Dataset-solun arvo < | < | < |
Miten v3.539.47 tekee HTML-entiteettien dekoodauksesta yksivaiheisen
PDFlibPas v3.539.47 tekee entiteettidekoodauksesta yksivaiheisen kolmella yhteen sovitetulla muutoksella: parseri dekoodaa &in viimeisenä, piirtovaihe ei enää dekoodaa mitään, ja jokainen paikka, joka kääntää dekoodatut sanat takaisin HTML:ksi, escapaa ne ensin uudelleen
Tekstisisällön tuettu entiteettijoukko on nyt <, >, & ja . Kaikki muu, mukaan lukien numeeriset viittaukset kuten A ja nimetyt entiteetit kuten ", pysyy kirjaimellisena tekstinä. Kyseinen raja merkitsee paljon sille, miten escapaaat omaa syötettäsi, kuten alla näkyy
Järjestys dekooderin sisällä on ensimmäinen korjaus. Jos & dekoodattaisiin ensin, syöte &lt; muuttuisi muodoksi < ja seuraava korvaus kääntäisi sen muotoon < — kaksoisdekoodaus, joka tapahtuu yhden vaiheen sisällä. ANSI-sanapolku korvaa siksi muodot <, > ja ensin ja &in viimeisenä, joten sen tuottamaa et-merkkiä ei koskaan tutkita uudelleen. UTF-16-sanapolku on yksi vasemmalta oikealle kulkeva pyyhkäisy kaksitavuisin askelin, joka kirjoittaa jokaisen osuman paikalleen ja siirtyy sen ohi, mikä antaa saman takeen rakenteellisesti
Toinen korjaus poistaa myöhäisen in korvauksen piirtovaiheesta. Dekoodaus kuuluu parserille eikä kenellekään muulle, joten sana, joka saapuu rivinjakajalle, on lopullista tekstiä
Kolmas korjaus on rajapintasääntö. NormalizeParsedHTML escapaa nyt muodot &, < ja > jokaisesta dekoodatusta sanasta ennen sen liittämistä normalisoituun HTML:ään. Toinen jäsennys dekoodaa sen takaisin täsmälleen samaksi tekstiksi, joten kokonaisvaikutus koko putken yli on yksi dekoodaus. Jatkomerkkijono noudattaa samaa sääntöä: sanat, jotka eivät mahtuneet laatikkoon, escapataan ennen niiden liittämistä muuttujaan LeftOverText, ja loput jäljelle jäävästä kopioidaan normalisoidusta HTML:stä, joka on jo escapatussa muodossa. Jälkiä sanoja keräävällä silmukalla on nyt myös sanamäärään sidottu raja, kun vanha repeat-silmukka saattoi astua viimeisen sanan yli
Miksi UTF-16BE-escapaus ei voi käyttää tavutason korvausta?
UTF-16BE-escapaus ei voi käyttää tavutason korvausta, koska et-merkin kaksitavuinen kuvio voi ulottua kahden toisiinsa liittymättömän merkin yli. Ainoa oikea työpilkku on koko 16-bittinen koodiyksikkö
Rendereri tallettaa Unicode-sanat big-endian UTF-16:nä paketoituna tavumerkkijonoihin, korkeampi tavu ensin. Et-merkki on 00 26. Ota nyt U+0100 (latinalainen iso A macronilla, tavut 01 00) ja sen perään U+2603 (lumimies, tavut 26 03). Tavujono on 01 00 26 03, ja tavut kaksi ja kolme lukevat 00 26. Tavuhaku muodolle #0'&' löytää et-merkin, jota ei ole olemassa, liittää &in tavut kahden merkin keskelle ja siirtää jokaista seuraavaa merkkiä yhdellä tavulla
Kyse ei ole eksoottisesta reunatapauksesta. Mikä tahansa merkki, jonka alhainen tavu on nolla, kelpaa toisen puolen toimittajaksi; U+4E00, yksi yleisimmistä CJK-merkeistä, täyttää ehdot. Kulmasulkeilla on sama haavoittuvuus: 00 3C ja 00 3E ilmestyvät aina, kun tällaisen merkin jälkeen tulee yksi merkki väliltä U+3C00–U+3EFF CJK Extension A:sta. Korjaus funktiossa EscapeHTMLWord purkaa tavut WideStringiksi, escapaa merkki kerrallaan ja paketoi tuloksen uudelleen. Dekooderin puoli oli jo turvallinen, koska se testaa kuvioita vain parillisilla koodiyksikkörajoilla
Sama sääntö pätee omaan koodiisi. Jos joskus pidät UTF-16-tekstiä TBytesina, esimerkiksi kutsun TEncoding.BigEndianUnicode.GetBytes jälkeen, älä etsi siitä tavukuvioita. Muunna takaisin merkkijonoksi ja työskentele merkeillä
Markdownin koodilohkot ja dataset-viennit: escapaa et-merkki ensin
Versiosta v3.539.47 alkaen molemmat PDFlibPasin sisäiset HTML-tuottajat, Markdown-muunnin ja dataset-viejä, escapaa et-merkin ennen kulmasulkeita, joten rendererin yksittäinen dekoodaus palauttaa täsmälleen alkuperäisen tekstin
Funktiossa MarkdownToHTML inline-koodialueet ja aitatut tai sisennetyt koodilohkot kuvaavat nyt muodon & muotoon &, muodon < muotoon < ja muodon > muotoon >, välilyönnit muuttuvat muodoksi ja sarkain neljäksi niistä sisennyksen säilyttämiseksi. Tavallinen Markdown-proosa escapaa vain kulmasulkeet, joten proosan raaka HTML ei voi ruiskuttaa tageja, mutta kirjoittaja voi yhä kirjoittaa muodon & tahallaan, aivan kuten Markdown-kirjoittajat odottavat. DrawMarkdownText ja DrawMarkdownTextBox käyttävät samaa muunnosta, joten koodi ilmestyy PDF:ään täsmälleen kirjoitetussa muodossa:
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
// Tutki HTML:ää: koodissa '&' muuttuu muodoksi '&' ja '<' muodoksi '<'
Html := Lib.MarkdownToHTML(Md);
Lib.SetOrigin(1); // vasen yläkulma alkupisteenä, Y kasvaa alaspäin
Lib.SetMeasurementUnits(0); // pisteet
// Sivu näyttää koodin täsmälleen kirjoitetussa muodossa, entiteettimuodot mukaan lukien
Lib.DrawMarkdownText(50, 50, 495, Md);
Lib.SaveToFile('code-sample.pdf');
finally
Lib.Free;
end;
end;
Dataset-viejä on opettavainen tapaus. Ennen v3.539.47 se escapasi vain kulmasulkeet, ja tahallaan: rendereri ei dekoodannut muotoa &, joten et-merkin escapaminen olisi tulostanut muodon & jokaiseen sellaisen sisältävään soluun. Kiertotie oli oikea vanhalle rendererille mutta väärä yleisesti, sillä soluarvo, joka sattui sisältämään muodon <, dekoodautui muodoksi <. Kun rendereri korjattiin, viejä escapaa muodon & ensin, ja arvo kuten R&D < & päätyy PDF:ään sellaisenaan. Jos rakennat raportit noin, läpikäynti artikkelissa TDataSetin vienti PDF-raportiksi Delphissä kattaa viejän loput
Se, miksi et-merkin on mentävä ensin, on syytä selittää kerran. Escapaa < ensin, ja saat muodon <; escapaa & toisena, ja siitä tulee &lt;, jonka oikea yksittäinen dekoodaus näyttää muodossa < muodon < sijaan. Peräkkäinen korvausketju on oikea vain, kun escape-merkki itse käsitellään ennen kaikkea muuta, mikä sitä tuottaa
Miten escapata luottamaton teksti DrawHTMLTextBoxia varten?
PDFlibPasin HTML-renderöinnissä escapata luottamaton tekstisisältö korvaamalla &, sitten <, sitten >, täsmälleen kerran, ja pidä luottamaton data kokonaan poissa attribuuttiarvoista
uses
System.SysUtils, PDFlibrary;
// Escapaa luottamaton teksti PDFlibPasin HTML-tekstisisältöä varten.
// '&' on korvattava ensin, muuten jo tuotetun '<':n sisällä
// oleva et-merkki escapataan toistamiseen
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;
Versiossa v3.539.47 kommentti kuten Try <a href="https://example.com">this</a> & <b> ilmestyy sivulle merkki merkiltä. Ennen v3.539.47 sama escapattu syöte saattoi tuottaa elävän linkkiannotaation, ja juuri se kääntää näyttövian tietoturvaongelmaksi: tikettikommentin ei pidä koskaan saada istuttaa napsautettavaa URL:ia asiakirjaan, johon henkilökuntasi luottaa
Huomaa, mitä funktio ei escapaa. Yleiskäyttöiset HTML-escaperit muuntavat myös muodon " muotoon " ja muodon ' muotoon ', mikä on oikein selaimelle. PDFlibPasin tekstin dekoodaus tunnistaa vain aiemmin luetellut neljä entiteettiä, joten kyseiset kaksi tulostuisivat kirjaimellisesti muodossa " ja '. Lainausmerkit ovat harmitonta tekstisisältöä; ne merkitsevät vain attribuuttiarvojen sisällä, eikä rendereri dekoodaa entiteettejä attribuuteissa yhtään. Turvallinen suunnittelu on siksi ei parempi escaper vaan sääntö: luottamatonta dataa ei koskaan viedä kohteisiin href, src tai style. Jos linkkikohdan on aidosti tultava käyttäjädasta, validoi se itse skeemojen ja merkkien sallittujen arvojen luetteloa vasten ja hylkää kaikki, mikä sisältää lainausmerkkejä tai kulmasulkeita
Kaksi päivityshuomiota seuraa korjauksesta suoraan:
- Jos koodisi lopetti muodon
&escapamisen, koska vanhemmat versiot tulostivat muodon&kirjaimellisena, lisää se takaisin. Ilman sitä käyttäjäteksti, joka sisältää muodon<, näkyy nyt muodossa<, yhä harmitonta tekstiä mutta ei enää sitä, mitä käyttäjä kirjoitti - Älä escapaa kahdesti. Teksti, joka kulkee kahden escaperin läpi, renderöi muodon
<näkyvänä muotona<, joten etsi se yksi rajapinta, jossa dataasi siirtyy HTML:ään, ja escapaa vain siellä
Sivutus LeftOverTextilla rikkomatta escapauksia
DrawHTMLTextBox palauttaa HTML:n, joka ei mahtunut, yleensä nimellä LeftOverText, ja versiosta v3.539.47 alkaen jäljelle jäävä osa säilyttää kirjaimelliset entiteettimuodot ja escapetut kulmasulkeet, kun viet sen seuraavaan laatikkoon. Sääntö kutsujille on yksinkertainen: vie se takaisin muuttumattomana
const
BoxLeft = 50;
BoxTop = 50;
BoxWidth = 495; // mitoitettu A4-sivulle pisteinä
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 on jo escapattua moottorin HTML:ää: älä koskaan escapaa tai unescapaa sitä
Rest := Lib.DrawHTMLTextBox(BoxLeft, BoxTop, BoxWidth, BoxHeight, Rest);
end;
if Rest <> '' then
raise Exception.CreateFmt('Content still left after %d pages', [MaxPages]);
end;
Käsittele jäljelle jäävää osaa läpinäkymättömänä. Se on moottorin normalisoitua HTML:ää, tyylit jo ratkaistuina, joten älä aja sitä oman escaperisi läpi, älä dekoodaa sitä äläkä liimaa siihen käyttäjätekstiä. Sivukatto on halpa vakuutus: jos jokin elementti ei koskaan mahdu laatikkoon, kattamattomalla silmukalla ei ole luonnollista poistumista
Markdownilla on oma jatkonsa. DrawMarkdownTextBox palauttaa tokenin, joka alkaa sisäisellä merkillä, jotta seuraava kutsu voi ohittaa muunnoksen; anna se takaisin funktiolle DrawMarkdownTextBox tai DrawMarkdownText, ei HTML-sisääntulopisteille, jotka piirtäisivät merkin tekstinä
Yleinen oppitunti: dekoodaa kerran, enkoodaa uudelleen jokaisella rajapinnalla
Jokaisen putken, joka jäsentää tekstiä, serialisoi tuloksen takaisin samaan syntaksiin ja jäsentää sen uudelleen, on kohdeltava dekoodausta operationa, joka tapahtuu täsmälleen yhdessä paikassa, ja sen on enkoodattava uudelleen jokaisella rajapinnalla, jossa dekoodattu teksti muuttuu takaisin syntaksiksi. Templaattimoottorit, HTML-sanitizerit ja Markdown–HTML–PDF-ketjut jakavat tämän muodon ja epäonnistuvat samalla tavalla, kun serialisoija unohtaa tuottavansa merkintää
Oireet ovat ennustettavissa, kun tunnet muodon. Liian vähäinen uudelleenkoodaus kääntää dataa syntaksiksi, mikä on injektiosuunta. Liiallinen koodaus tai kahdesti ajettava dekooderi näyttää entiteettimuodot lukijalle tai nielaisee ne, mikä on näyttösuunta. Vain toisen suunnan korjaaminen rikkoo yleensä toisen, minkä vuoksi PDFlibPasin korjauksen on täytynyt lisätä &in dekoodaus, järjestää se uudelleen, poistaa myöhäinen dekoodaus ja lisätä uudelleenescapaus samassa julkaisussa. Sama periaate kulkee toiseen suuntaan, kun PDF-sisältö viedään jäsenneltynä tekstinä, kuten artikkelissa PDF:n semanttinen vienti Markdowniksi ja DOCX:ksi Delphista, jossa jokainen kirjaimellinen merkki on escapattava kohdesyntaksia varten täsmälleen kerran
Pikamuistilista
- Päivitys PDFlibPas v3.539.47:ään tai uudempaan, jos renderöit käyttäjädataa sisältävää HTML:ää tai Markdownia
- Escapaa tekstisisältö ensin muodolla
&, sitten<ja>; älä muunna lainausmerkkejä PDFlibPasin tekstiä varten - Escapaa kerran, siinä yhdessä kohdassa, jossa data siirtyy HTML-merkkijonoon
- Pidä luottamattomat arvot poissa kohteista
href,srcjastyle, tai validoi ne sallittujen arvojen luetteloa vasten - Oleta, että vain muodot
<,>,&ja dekoodataan tekstissä; muut entiteetit pysyvät kirjaimellisina - Vie
LeftOverTexttakaisin funktiolleDrawHTMLTextBoxmuuttumattomana ja aseta sivusilmukalle katto - Anna Markdown-jatkotokenit vain funktioille
DrawMarkdownTextBoxtaiDrawMarkdownText - Älä koskaan etsi UTF-16-tavupuskureista tavukuvioita; työskentele kokonaisten koodiyksiköiden kanssa
HTML- ja Markdown-renderöinti, dataset-raporttien vienti ja asettelumoottorin loput toimitetaan PDF Library for Delphille natiivina Pascal-lähdekoodina Delphille ja Free Pascalille. Katso PDFlibPas-tuotesivulta versiot, alustatuki ja kokeiluversion lataus