A Delphi PDF Library (PDFlibPas) v3.539.47 előtti verziói kétszer is dekódolhatták az escape-elt szöveget, amikor HTML-t vagy Markdown-t rajzoltak PDF-be. A DrawHTMLText és a DrawHTMLTextBox először értelmezi a HTML-t, majd visszaalakítja HTML-lé, és újra értelmezi, így a <unsafe> alakban írt szöveg a második értelmezéshez már valódi tagként érkezett meg. A v3.539.47 óta minden entitás pontosan egyszer dekódolódik, és mindenhol újra escape-elésre kerül, ahol a szöveg visszaalakul HTML-lé
A helyzet, ami kirobbantja ezt, hétköznapi. Egy help desk PDF-be exportálja a ticketeket, és az ügyfél megjegyzése egy HTML sablonba kerül. A fejlesztő helyesen járt el, és escape-elte a megjegyzést, így a <b> <b> lett. A renderelőben ez az escape-elés csendben visszavonódott: a megjegyzés félkövérülve jelent meg, egy ismeretlen tagnév egyszerűen eltűnt az oldalról, az escape-elt horgony pedig kattintható link annotációvá alakult. Nincs exception, nincs figyelmeztetés, csak egy tökéletesen érvényes PDF, ami nem azt írja, amit az adat
Miért válik az escape-elt szöveg valódi taggá a PDF-ben?
Az escape-elt szöveg azért lett markup, mert a renderelő két értelmezési menetben fut, és a köztes normalizálási lépés a már dekódolt szöveget escape-elés nélkül írta vissza HTML-lé. Minden dekódolás, amit az első menet elvégzett, a második menet számára élő szintaxisként állt rendelkezésre
A két menetnek jó oka van. Az első menet a tag- és szóelemek listáját építi fel. A NormalizeParsedHTML ezután feloldja a stíluslap-kaszkádot: a <style> blokkok szabályait illeszti az egyes tagekhez, összevonja őket az inline style attribútumokkal, az eredményt a tagre írja, és a teljes elemlistát visszaírja HTML stringként. A layout menet ezt a normalizált stringet értelmezi. Ugyanez a gépezet hajtja a flexboxot, a CSS gridet és a lábjegyzet-elrendezést a PDFlibPas HTML renderelésében
A hiba a szavak szerializálásában volt. A tagek az eredeti forrásformájukban íródtak vissza, a szavak viszont a dekódolt formájukban. Egy szó, amit az első menet <unsafe>-ből <unsafe>-ra dekódolt, nyers szögletes zárójelekként landolt a normalizált HTML-ben, és a második menet eleme olvasta. Ehhez a főhibához három kisebb szivárgás társult, ugyanabba az irányba mutatva:
- A
&nem szerepelt a támogatott entitáskészletben, így azR&Dszó szerint nyomtatódott, és nem volt mód arra, hogy valaki egy literal entitásírásmódot, mondjuk<-t, szövegként leírjon - A rajzolási szakasz másodszorra is kicserélte a
-t, már az értelmezés befejezése után, így egy literal entitásírás a legvégén is eltűnhetett - A Markdown kód escape-elése kihagyta az ampersandot, a dataset exportáló pedig csak a szögletes zárójeleket escapeelte, így a kódban vagy cellaértékekben lévő entitásírások markupként dekódolódtak
| A renderelőhöz érkező bemenet | v3.539.47 előtt | v3.539.47 óta |
|---|---|---|
<unsafe> | Tagként értelmeződik, a szöveg sosem jut az oldalra | <unsafe> szövegként kirajzolva |
<b>x</b> | x félkövéren kirajzolva | <b>x</b> szövegként kirajzolva |
R&D | R&D szó szerint nyomtatva | R&D |
&lt; | &lt; szó szerint nyomtatva | < |
-t tartalmazó Markdown kódszakasz | Nem törhető szóközzé vált | szövegként kirajzolva |
Dataset cellaérték: < | < | < |
Hogyan teszi a v3.539.47 egylépésessé a HTML entitásdekódolást?
A PDFlibPas v3.539.47 három összehangolt változtatással teszi egylépésessé az entitásdekódolást: a parser legutolsóként dekódolja az &-ot, a rajzolási szakasz már semmit sem dekódol, és minden hely, ami a dekódolt szavakat visszaírja HTML-lé, előbb újra escape-eli őket
A szövegtartalom támogatott entitáskészlete mostantól <, >, & és . Bármi más, így a numerikus hivatkozások, mint az A, és a nevesített entitások, mint a ", literal szöveg marad. Ez a határ azért számít, amikor a saját bemenetedet escape-eled, ahogy az alább látható
A dekódolon belüli sorrend az első javítás. Ha az & dekódolódna először, a &lt; bemenet <-vé válna, és a következő csere <-re fordítaná: ez egy dupla dekódolás, ami egyetlen meneten belül történik. Az ANSI szóút ezért előbb az <-t, az >-t és az -t cseréli, az &-ot pedig utoljára, így az általa előállított ampersandot már senki nem nézi meg újra. Az UTF-16 szóút egyetlen balról jobbra szkennelés kétbájtos lépésekben, ami minden találatot helyben átír, és átugrik fölötte, ami strukturálisan ugyanezt a garanciát adja
A második javítás kihúzza a késői cserét a rajzolási szakaszból. A dekódolás a parser dolga, és senki másé, így ami a sortörőhöz érkezik, az végső szöveg
A harmadik javítás a határszabály. A NormalizeParsedHTML mostantól minden dekódolt szóban escape-eli a &-ot, a <-t és a >-t, mielőtt hozzáfűzné a normalizált HTML-hez. A második értelmezés pontosan ugyanarra a szövegre dekódolja vissza, így a teljes futószalagon átvetítve a nettó hatás egyetlen dekódolás. A folytatólagos string ugyanezt a szabályt követi: a dobozba be nem férő szavak escape-elve kerülnek a LeftOverText-be, a maradék többi része pedig a normalizált HTML-ből másolódik, ami már escape-elt formában van. Azokat a maradék szavakat összegyűjtő ciklus mostantól a szószámmal is határolt, ahol a régi repeat ciklus átléphetett az utolsó szón
Miért nem oldható meg az UTF-16BE escape-elés bájtszintű cserével?
Az UTF-16BE escape-elés nem oldható meg bájtszintű cserével, mert az ampersand kétbájtos mintája két egymással semmi kapcsolatban nem lévő karakteren át is ívelhet. Az egyetlen helyes munkaegység a teljes 16 bites kódegység
A renderelő a Unicode szavakat big-endian UTF-16-ként tárolja bájtstringekbe pakolva, magas bájt elől. Egy ampersand 00 26. Most vedd az U+0100-at (macronos nagy A latin, bájtjai 01 00), amelyet az U+2603 követ (a hóember, bájtjai 26 03). A bájsorozat 01 00 26 03, a második és harmadik bájt pedig 00 26-ot ad. Egy #0'&'-ra kereső bájtkeresés olyan ampersandot talál, ami nem létezik, az & bájtjait két karakter közepébe varrja, és minden következő karaktert eggyel bájttal elnyír
Ez nem egzotikus sarokeset. Bármely karakter, aminek az alacsony bájtja nulla, szolgáltathatja az első felét; az U+4E00, az egyik leggyakoribb CJK ideográfia, megfelel. A szögletes zárójelek ugyanígy ki vannak téve: a 00 3C és a 00 3E bármikor megjelenik, amikor ilyen karaktert a CJK Extension A U+3C00-től U+3EFF-ig terjedő tartományából való karakter követ. A javítás az EscapeHTMLWord-ben a bájtokat WideString-gé bontja ki, karakterenként escape-el, és újra becsomagolja az eredményt. A dekódoló oldal már biztonságos volt, mert csak páros kódegység-határokon tesztel mintákat
Ugyanez a szabály érvényes a saját kódodra is. Ha valaha UTF-16 szöveget tartasz TBytes-ként, mondjuk TEncoding.BigEndianUnicode.GetBytes után, ne keress rajta bájtmintákat. Alakítsd vissza stringgé, és karaktereken dolgozz
Markdown kódblokkok és dataset exportok: előbb az ampersandot escape-el
A v3.539.47 óta a PDFlibPas mindkét HTML előállítója, a Markdown konverter és a dataset exportáló, a szögletes zárójelek előtt escape-eli az ampersandot, így a renderelő egyetlen dekódolása pontosan az eredeti szöveget állítja vissza
A MarkdownToHTML-ban az inline kódszakaszok és a keretezett vagy behúzott kódblokkok mostantól az &-t &-ra, a <-t <-re és a >-t >-re képezik, a szóközök pedig -vé válnak, a tab négy ilyenné, hogy a behúzás megmaradjon. A közönséges Markdown próza csak a szögletes zárójeleket escape-eli, így a prózában lévő nyers HTML nem injektálhat taget, miközben a szerző továbbra is szándékosan írhat &-ot, nagyjából úgy, ahogy a Markdown szerzők elvárják. A DrawMarkdownText és a DrawMarkdownTextBox ugyanezt a konverziót használja, így a kód a PDF-ben pontosan úgy jelenik meg, ahogy begépték:
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
// A HTML megszemlélése: kódban a '&' '&' lesz, a '<' pedig '<'
Html := Lib.MarkdownToHTML(Md);
Lib.SetOrigin(1); // bal felső origó, az Y lefelé nő
Lib.SetMeasurementUnits(0); // pontok
// Az oldal a kódot pontosan begépelve mutatja, entitásírásokkal együtt
Lib.DrawMarkdownText(50, 50, 495, Md);
Lib.SaveToFile('code-sample.pdf');
finally
Lib.Free;
end;
end;
A dataset exportáló a tanulságos eset. A v3.539.47 előtt csak a szögletes zárójeleket escapeelte, és szándékosan: a renderelő nem dekódolta az &-ot, így az ampersand escape-elése minden egyet tartalmazó cellában &-ot nyomtatott volna. A workaround a régi renderelőre nézve helyes volt, általánosságban viszont téves, mert egy véletlenül <-t tartalmazó cellaérték <-re dekódolódott. A renderelő javításával az exportáló előbb escape-eli az &-ot, és egy R&D < & érték szó szerint landol a PDF-ben. Ha így építesz riportokat, a TDataSet PDF riportba exportálását bemutató átvezetés Delphiben az exportáló többi részét is tárgyalja
Azt, miért kell az ampersandnak elöl mennie, érdemes egyszer leírni. Ha előbb a <-t escape-eled, <-t kapsz; ha másodikként az &-t escape-eled, abból &lt; lesz, amit egy helyes egyszeri dekódolás <-ként jelenít meg < helyett. A szekvenciális csrelánc csak akkor helyes, ha magát az escape karaktert kezelik elöbb, mindannál, ami bevezeti
Hogyan escape-elj megbízhatatlan szöveget a DrawHTMLTextBox-hoz?
A PDFlibPas HTML rendereléséhez a megbízhatatlan szövegtartalmat úgy escape-eld, hogy az &-ot, majd a <-t, majd a >-t cseréled, pontosan egyszer, a megbízhatatlan adatot pedig teljesen tartsd távol az attribútumértékektől
uses
System.SysUtils, PDFlibrary;
// Megbízhatatlan szöveg escape-elése PDFlibPas HTML szövegtartalomhoz.
// A '&' cseréje elsőnek kell, hogy essék, különben a már
// legyártott '<' ampersandja másodszorra is escape-elődne
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;
A v3.539.47-en egy Try <a href="https://example.com">this</a> & <b> megjegyzés karakterenként pontosan jelenik meg az oldalon. A v3.539.47 előtt ugyanez az escape-elt bemenet élő link annotációt gyárthatott, és ez az, ami egy megjelenítési bakit biztonsági problémává fokoz: egy ticket megjegyzése sosem ültethet kattintható URL-t egy olyan dokumentumba, amiben a kollégáid megbíznak
Vedd észre, mit nem escape-el a függvény. Az általános célú HTML escape-elők az idézőjeleket is konvertálják, a "-t "-ra és a '-t '-re, ami böngészőben helyes. A PDFlibPas szövegdekódolása csak a fent felsorolt négy entitást ismeri, így ez a kettő literal szövegként nyomtatódna, mint " és '. Az idézőjelek a szövegtartalomban ártalmatlanok; csak az attribútumértékekben számítanak, és a renderelő az attribútumokban egyáltalán nem dekódol entitásokat. A biztonságos tervezés ezért nem egy jobb escape-elő, hanem egy szabály: megbízhatatlan adat sosem kerül href-be, src-be vagy style-ba. Ha egy link célja tényleg felhasználói adatból jön, magad validáld egy engedélyezett sémákból és karakterekből álló listához, és utasíts el mindent, ami idézőjelet vagy szögletes zárójelet tartalmaz
Két frissítési megjegyzés következik közvetlenül a javításból:
- Ha a kódod abbahagyta az
&escape-elését, mert a régebbi verziók szó szerint nyomtatták az&-ot, add vissza. Nélküle a<-t tartalmazó felhasználói szöveg mostantól<-ként jelenik meg: még mindig ártalmatlan szöveg, de már nem az, amit a felhasználó gépelt - Ne escape-elj kétszer. Két escape-előn átesett szöveg a
<-t látható<írásmódként rendereli, ezért keresd meg azt az egyetlen határt, ahol az adataid belépnek a HTML-be, és csak ott escape-elj
Lapozás LeftOverText-tel az escape-ek megtörése nélkül
A DrawHTMLTextBox visszaadja azt a HTML-t, ami nem fért bele, és amit általában LeftOverText-nek hívnak; a v3.539.47 óta ez a maradék megőrzi a literal entitásírásokat és az escape-elt szögletes zárójeleket, amikor a következő doboznak adod át. A hívók szabálya egyszerű: add tovább változatlanul
const
BoxLeft = 50;
BoxTop = 50;
BoxWidth = 495; // A4 oldalra méretezve pontokban
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);
// A LeftOverText már escape-elt motor HTML: sosem escape-eled vagy dekódolod
Rest := Lib.DrawHTMLTextBox(BoxLeft, BoxTop, BoxWidth, BoxHeight, Rest);
end;
if Rest <> '' then
raise Exception.CreateFmt('Content still left after %d pages', [MaxPages]);
end;
A maradékot kezeld opaknak. Ez a motor normalizált HTML-je, a stílusok már fel vannak benne oldva, ezért ne vezesd át a saját escape-előden, ne dekódold, és ne varrj bele felhasználói szöveget. Az oldalplafon olcsó biztosítás: ha egy elem sosem fér bele a dobozba, a le nem korlátozott ciklusnak nincs természetes kijárata
A Markdown-nak saját folytatása van. A DrawMarkdownTextBox egy belső markerrel induló tokent ad vissza, hogy a következő hívás átugorhassa a konverziót; add vissza a DrawMarkdownTextBox-nak vagy a DrawMarkdownText-nek, ne a HTML belépési pontoknak, mert azok a markert szövegként rajzolnák
A tágabb tanulság: dekódolj egyszer, kódolj újra minden határon
Minden futószalag, ami szöveget értelmez, az eredményt visszaírja ugyanabba a szintaxisba, majd újra értelmezi, a dekódolást pontosan egy helyen megtörténő műveletként kezelje, és minden határon kódoljon újra, ahol a dekódolt szöveg újra szintaxissá válik. A template motorok, a HTML szanitálók és a Markdown-HTML-PDF láncok ugyanezt a formát hordozzák, és ugyanígy buknak el, amikor egy szerializáló elfelejti, hogy markupot gyárt
A tünetek kitalálhatók, ha ismered a formát. A túl kevés újrakódolás az adatot szintaxissá teszi, ez a befecskendezés iránya. A túl sok kódolás, vagy a kétszer lefutó dekódoló entitásírásokat mutat az olvasónak, vagy felfalja őket, ez a megjelenítés iránya. Az egyik irány önmagában való javítása általában eltöri a másikat, ezért a PDFlibPas javításának ugyanabban a kiadásban kellett hozzáadnia az & dekódolást, átrendeznie azt, kihúznia a késői dekódolást és hozzáadnia az újraescape-elést. Ugyanez az elv fordítva is fut, amikor PDF tartalom exportálódik strukturált szövegként, mint a PDF-ből Markdown és DOCX szemantikus export Delphiből esetén, ahol minden literal karaktert pontosan egyszer kell escape-elni a cél szintaxisra
Gyorsreferencia-ellenőrzőlista
- Frissíts PDFlibPas v3.539.47-re vagy újabbra, ha felhasználói adatot tartalmazó HTML-t vagy Markdown-t renderelsz
- A szövegtartalmat az
&-tal escape-eld előbb, majd a<-t és a>-t; az idézőjeleket ne konvertáld PDFlibPas szöveghez - Escape-elj egyszer, annál az egyetlen pontnál, ahol az adat belép a HTML stringbe
- Tartsd a megbízhatatlan értékeket távol a
href-től, asrc-től és astyle-tól, vagy validáld őket engedélylistához - Csak a
<,>,&és dekódolására számíthatsz szövegben; a többi entitás literal marad - A
LeftOverText-et változatlanul add vissza aDrawHTMLTextBox-nak, és korlátozd az oldalciklust - A Markdown folytatólagos tokeneket csak a
DrawMarkdownTextBox-nak vagy aDrawMarkdownText-nek add át - Sose keress bájtmintákat UTF-16 bájtpufferen; egész kódegységeken dolgozz
A HTML és Markdown renderelés, a dataset riportexport és a layout motor többi része a Delphi PDF Library natív Pascal forrásában érkezik, Delphihez és Free Pascalhoz. Lásd a PDFlibPas termékoldalt a kiadásokért, platformtámogatásért és trial letöltésért