PDF Library for Delphi (PDFlibPas) v3.539.47 öncesi sürümler, HTML ya da Markdown'ı PDF'e çizerken kaçırılmış metni iki kez çözebiliyordu. DrawHTMLText ve DrawHTMLTextBox HTML'i ayrıştırır, onu HTML'e geri normalize eder ve sonra yeniden ayrıştırır; dolayısıyla <unsafe> yazılmış metin ikinci ayrıştırmaya gerçek bir tag olarak ulaşıyordu. v3.539.47'den beri her entity tam olarak bir kez çözülür ve metin, HTML'e geri döndüğü her yerde yeniden escape edilir
Bunu ortaya çıkaran senaryo sıradandır. Bir help desk talepleri PDF'e aktarır ve müşteri yorumu bir HTML şablonuna girer. Geliştirici doğru olanı yaptı ve yorumu escape etti; <b>, <b> oldu. Renderer'ın içinde o kaçış sessizce geri alındı: yorum bold çıktı, bilinmeyen bir tag adı sayfadan sade bir şekilde kayboldu ve escape edilmiş bir anchor tıklanabilir bir link annotation'a dönüştü. Ne istisna ne uyarı; veriden farklı bir şey söyleyen kusursuzca geçerli bir PDF
Kaçırılmış metin neden PDF'te gerçek bir tag'e dönüşür?
Kaçırılmış metin markup oldu, çünkü renderer iki ayrıştırma geçişi çalıştırıyor ve aralarındaki normalizasyon adımı, hâlihazırda çözülmüş metni yeniden escape etmeden HTML'e geri yazıyordu. İlk ayrıştırmanın yaptığı her çözüm, böylece ikinci ayrıştırmaya canlı sözdizimi olarak sunuluyordu
İki geçişin var olmasının iyi bir nedeni var. İlk ayrıştırma tag ve kelime elementlerinden oluşan bir liste kurar. NormalizeParsedHTML sonra stylesheet cascade'ini çözer: <style> bloklarından gelen kuralları her tag ile eşleştirir, inline style attribute'larıyla birleştirir, sonucu tag üzerinde saklar ve bütün element listesini bir HTML string'ine geri serileştirir. Layout geçişi o normalize edilmiş string'i ayrıştırır. PDFlibPas HTML render'ında flexbox, CSS grid ve footnote layoutu süren de aynı makinedir
Kusur, kelimelerin nasıl serileştirildiğindeydi. Tag'ler özgün kaynak biçimlerinden geri yazılırken kelimeler çözülmüş biçimleriyle geri yazılıyordu. İlk ayrıştırmanın <unsafe>ten <unsafe>'e çözdüğü bir kelime, normalize edilmiş HTML'e çıplak açı parantezleri olarak düşüyor ve ikinci ayrıştırma onu bir element olarak okuyordu. O çekirdek hatanın etrafında aynı yönü gösteren üç daha küçük sızıntı duruyordu:
&desteklenen entity setinde yoktu; dolayısıylaR&Dliteral basılıyor ve<gibi bir literal entity yazımını metin olarak yazmanın yolu yoktu- Çizim aşaması
'i, ayrıştırma çoktan bitmişken ikinci kez değiştiriyordu; böylece bir literal entity yazımı en sonda hâlâ kaybolabiliyordu - Markdown kod kaçışı ampersand'ı atlıyor, dataset exporter'ı yalnızca açı parantezlerini escape ediyordu; dolayısıyla kod içindeki ya da hücre değerlerindeki entity yazımları markup olarak çözülüyordu
| Renderer'a ulaşan girdi | v3.539.47 öncesi | v3.539.47'den beri |
|---|---|---|
<unsafe> | Tag olarak ayrıştırıldı, metin sayfaya hiç ulaşmadı | <unsafe> metin olarak çizilir |
<b>x</b> | x bold çizildi | <b>x</b> metin olarak çizilir |
R&D | R&D literal basıldı | R&D |
&lt; | &lt; literal basıldı | < |
içeren Markdown code span | Non-breaking space'e dönüştü | metin olarak çizilir |
Dataset hücre değeri < | < | < |
v3.539.47 HTML entity çözümünü nasıl tek geçişli yapar?
PDFlibPas v3.539.47, entity çözümünü üç eşgüdümlü değişiklikle tek geçişli yapar: ayrıştırıcı &'i en son çözer, çizim aşaması artık hiçbir şeyi çözmez ve çözülmüş kelimeleri HTML'e geri çeviren her yer onları önce yeniden escape eder
Text content için desteklenen entity seti artık <, >, & ve 'dir. A gibi sayısal referanslar ile " gibi isimli entity'ler dahil başka her şey literal metin olarak kalır. O sınır, kendi girdinizi nasıl escape edeceğiniz için önemlidir; aşağıda görüldüğü gibi
Decoder'ın içindeki sıra ilk düzeltmedir. & önce çözülseydi &lt; girdisi < olurdu ve sonraki değiştirme onu <'e çevirirdi; tek geçişin içinde gerçekleşen bir çift çözüm. ANSI kelime yolu bu yüzden önce <, > ve 'i, &'i ise en son değiştirir; böylece ürettiği ampersand bir daha asla incelenmez. UTF-16 kelime yolu, her eşleşmeyi yerinde yeniden yazıp üzerinden geçen iki baytlık adımlarla yapılan tek bir soldan sağa taramadır; aynı garantiyi yapısal olarak verir
İkinci düzeltme, geç kalmış değiştirmesini çizim aşamasından çıkarır. Çözüm ayrıştırıcıya aittir ve kimseye değil; dolayısıyla satır kırıcısına ulaşan kelime nihai metindir
Üçüncü düzeltme sınır kuralıdır. NormalizeParsedHTML artık her çözülmüş kelimeyi normalize edilmiş HTML'e eklemeden önce içindeki &, < ve >'yi escape eder. İkinci ayrıştırma onu birebir aynı metne geri çözer; dolayısıyla bütün hat üzerindeki net etki bir çözümdür. Devam string'i aynı kuralı izler: kutuya sığmayan kelimeler LeftOverText'e eklenmeden önce escape edilir ve kalanın gerisi, hâlihazırda escape edilmiş biçimde olan normalize edilmiş HTML'den kopyalanır. O artık kelimeleri toplayan döngü de kelime sayısıyla sınırlıdır; eski repeat döngüsü son kelimenin ötesine adım atabiliyordu
UTF-16BE kaçışı neden bayt düzeyinde bir replace kullanamaz?
UTF-16BE kaçışı bayt düzeyinde bir replace kullanamaz, çünkü ampersand'ın iki baytlık deseni birbirinden bağımsız iki karakterin üzerine yayılabilir. Doğru olan tek iş birimi, bütün 16 bitlik code unit'tir
Renderer Unicode kelimeleri, yüksek bayt önde olacak biçimde big-endian UTF-16 olarak bayt string'lerine paketlenmiş hâlde saklar. Bir ampersand 00 26'dır. Şimdi U+0100'ı (makronlu Latin büyük A, baytları 01 00) ardından U+2603'ü (kardan adam, baytları 26 03) alın. Bayt dizisi 01 00 26 03'tür ve ikinci ile üçüncü baytlar 00 26 okur. #0'&' için yapılan bir bayt araması var olmayan bir ampersand bulur, &'in baytlarını iki karakterin ortasına ekler ve izleyen her karakteri bir bayt kaydırır
Bu egzotik bir uç durum değildir. Düşük baytı sıfır olan her karakter ilk yarıyı sağlayabilir; en sık görülen CJK ideograflarından U+4E00 bu tanıma uyar. Açı parantezleri de aynı maruziyettedir: 00 3C ile 00 3E, CJK Extension A'da U+3C00 ile U+3EFF arasından bir karakterin böyle bir karakteri izlemesi hâlinde görünür. EscapeHTMLWord'deki düzeltme baytları bir WideString'e açar, karakter karakter escape eder ve sonucu yeniden paketler. Decoder tarafı hâlihazırda güvendeydi, çünkü desenleri yalnızca çift code unit sınırlarında sınar
Aynı kural kendi kodunuz için de geçerlidir. UTF-16 metnini bir TBytes olarak tutuyorsanız, örneğin TEncoding.BigEndianUnicode.GetBytes sonrasında, onu bayt desenleri için aramayın. String'e geri çevirin ve karakterler üzerinde çalışın
Markdown code block'ları ve dataset dışa aktarımları: önce ampersand'ı escape edin
v3.539.47'den beri PDFlibPas'ın içindeki iki HTML üreticisi de, Markdown converter ile dataset exporter, ampersand'ı açı parantezlerinden önce escape eder; böylece renderer'daki tek çözüm özgün metni birebir geri getirir
MarkdownToHTML'da inline code span'ler ile fenced ya da girintili code block'lar artık &'i &'e, <'i <'e ve >'yi >'e eşler; boşluklar olur ve girintiyi korumak için bir tab dört tanesine dönüşür. Sıradan Markdown düzyazısı yalnızca açı parantezlerini escape eder; böylece düzyazıdaki ham HTML tag enjekte edemezken bir yazar &'i hâlâ kasıtlı yazabilir, tam da Markdown yazarlarının beklediği gibi. DrawMarkdownText ile DrawMarkdownTextBox aynı dönüşümü kullanır; dolayısıyla kod PDF'te yazıldığı gibi görünür:
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
// HTML'i inceleyin: kod içinde '&', '&' olur ve '<', '<' olur
Html := Lib.MarkdownToHTML(Md);
Lib.SetOrigin(1); // sol üst köken, Y aşağı doğru büyür
Lib.SetMeasurementUnits(0); // point
// Sayfa kodu yazıldığı gibi gösterir, entity yazımları dahil
Lib.DrawMarkdownText(50, 50, 495, Md);
Lib.SaveToFile('code-sample.pdf');
finally
Lib.Free;
end;
end;
Asıl öğretici örnek dataset exporter'dır. v3.539.47 öncesinde yalnızca açı parantezlerini escape ediyordu ve bu kasıtlıydı: renderer &'i çözmüyordu; dolayısıyla ampersand'ı escape etmek onu taşıyan her hücreye & basacaktı. Geçici çözüm eski renderer için doğruydu, genelde yanlış: < taşıyan bir hücre değeri <'e çözülüyordu. Renderer düzelince exporter önce &'i escape eder ve R&D < & gibi bir değer PDF'e birebir düşer. Raporları böyle kuruyorsanız Delphi'de bir TDataSet'i PDF raporuna aktarma gezintisi exporter'ın geri kalanını kapsar
Ampersand'ın neden önce gitmesi gerektiğini bir kez açıkça yazmaya değer. Önce <'i escape ederseniz < alırsınız; sonra &'i escape ederseniz bu &lt; olur ve doğru bir tek çözüm bunu < yerine < gösterir. Sıralı bir replace zinciri yalnızca escape karakterinin kendisi, onu üreten her şeyden önce ele alındığında doğrudur
DrawHTMLTextBox için güvenilmeyen metni nasıl escape etmelisiniz?
PDFlibPas HTML render'ında güvenilmeyen text content'i, &'i, sonra <'i, sonra >'yi tam bir kez değiştirerek escape edin ve güvenilmeyen veriyi attribute değerlerinin tamamen dışında tutun
uses
System.SysUtils, PDFlibrary;
// Güvenilmeyen metni PDFlibPas HTML text content'i için escape eder.
// '&' önce değiştirilmeli, yoksa hâlihazırda üretilmiş bir
// '<' içindeki ampersand ikinci kez escape edilirdi
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;
v3.539.47'de Try <a href="https://example.com">this</a> & <b> gibi bir yorum sayfada karakter karakter görünür. v3.539.47 öncesinde aynı escape edilmiş girdi canlı bir link annotation üretebiliyordu; görüntü bozukluğunu güvenlik sorununa çeviren kısım da budur: bir talep yorumu, personelinizin güvendiği bir belgeye tıklanabilir bir URL ekleyebilmemeli
Fonksiyonun neyi escape etmediğine dikkat edin. Genel amaçlı HTML escaper'ları ayrıca " işaretini "'e ve ' işaretini ''e çevirir; bu bir tarayıcı için doğrudur. PDFlibPas metin çözümü yalnızca yukarıda sayılan dört entity'yi tanır; dolayısıyla o ikisi " ile ' olarak literal basılırdı. Tırnaklar text content'te zararsızdır; yalnızca attribute değerlerinin içinde önem taşırlar ve renderer attribute'lardaki entity'leri hiç çözmüyor. Güvenli tasarım bu yüzden daha iyi bir escaper değil bir kuraldır: güvenilmeyen veri asla href'e, src'ye ya da style'a girmez. Bir link hedefi gerçekten kullanıcı verisinden gelmek zorundaysa kendiniz şema ve karakter allow-list'ine karşı doğrulayın ve tırnak ya da açı parantezi taşıyan her şeyi reddedin
Düzeltmeden doğrudan iki yükseltme notu çıkar:
- Kodunuz, eski sürümler
&'i literal bastığı için&'i escape etmeyi bıraktıysa onu geri ekleyin. Onsuz,<içeren kullanıcı metni artık<görüntüler; hâlâ zararsız metin ama artık kullanıcının yazdığı şey değil - İki kez escape etmeyin. İki escaper'dan geçen metin
<'i görünür<yazımıyla render eder; dolayısıyla verinizin HTML'e girdiği tek sınırı bulun ve yalnızca orada escape edin
LeftOverText ile sayfalama, kaçışları bozmadan
DrawHTMLTextBox sığmayan HTML'i döndürür; genelde adı LeftOverText'tir. v3.539.47'den beri o kalan, bir sonraki kutuya geçirdiğinizde literal entity yazımlarını ve escape edilmiş açı parantezlerini korur. Çağıranlar için kural basittir: onu değiştirmeden geri geçirin
const
BoxLeft = 50;
BoxTop = 50;
BoxWidth = 495; // point cinsinden A4 sayfa için boyutlandı
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 hâlihazırda escape edilmiş engine HTML'idir: asla escape etmeyin ya da geri çözmeyin
Rest := Lib.DrawHTMLTextBox(BoxLeft, BoxTop, BoxWidth, BoxHeight, Rest);
end;
if Rest <> '' then
raise Exception.CreateFmt('Content still left after %d pages', [MaxPages]);
end;
Kalanı opak sayın. O, stilleri hâlihazırda çözülmüş engine'in normalize edilmiş HTML'idir; dolayısıyla kendi escaper'ınızdan geçirmeyin, çözmeyin ve içine kullanıcı metni dikmeyin. Sayfa tavanı ucuz bir sigortadır: bir element kutuya hiç sığmayacaksa tavansız bir döngünün doğal bir çıkışı yoktur
Markdown'ın kendi devamı vardır. DrawMarkdownTextBox, içsel bir marker ile başlayan bir token döndürür; böylece sonraki çağrı dönüşümü atlayabilir. Onu DrawMarkdownTextBox'a ya da DrawMarkdownText'e verin, HTML giriş noktalarına değil; onlar marker'ı metin olarak çizerdi
Genel ders: bir kez çöz, her sınıtta yeniden kodla
Metni ayrıştıran, sonucu aynı sözdizimine geri serileştiren ve yeniden ayrıştıran her hattın çözümü tam olarak tek bir yerde gerçekleşen bir işlem sayması ve çözülmüş metnin yeniden sözdizime dönüştüğü her sınıtta yeniden kodlaması gerekir. Template engine'leri, HTML sanitizer'ları ve Markdown-to-HTML-to-PDF zincirleri aynı biçimi paylaşır ve bir serileştirici markup ürettiğini unuttuğunda aynı şekilde çöker
Biçimi bilince belirtiler öngörülebilir hâle gelir. Az yeniden kodlama veriyi sözdizimine çevirir; enjeksiyon yönü budur. Çok kodlama ya da iki kez koşan bir decoder ise entity yazımlarını okura gösterir ya da yer; görüntü yönü budur. Tek yönü düzeltmek genellikle ötekini bozar; PDFlibPas düzeltmesinin aynı sürümde & çözümünü eklemesi, sırasını değiştirmesi, geç çözümü kaldırması ve yeniden kaçışı eklemesi bu yüzden zorunluydu. Aynı ilke ters yönde de işler; PDF içeriği yapılandırılmış metin olarak dışa aktarıldığında, Delphi'den PDF-to-Markdown ve DOCX semantik dışa aktarmadaki gibi, her literal karakter hedef sözdizimi için tam bir kez escape edilmelidir
Hızlı başvuru listesi
- Kullanıcı verisi içeren HTML ya da Markdown render ediyorsanız PDFlibPas v3.539.47 ve sonrasına yükseltin
- Text content'i önce
&, sonra<ve>ile escape edin; PDFlibPas metni için tırnakları çevirmeyin - Bir kez escape edin, verinin HTML string'ine girdiği tek noktada
- Güvenilmeyen değerleri
href,srcvestyledışında tutun ya da bir allow-list'e karşı doğrulayın - Metinde çözülmeyi yalnızca
<,>,&ve 'den bekleyin; öbür entity'ler literal kalır LeftOverText'i değiştirmedenDrawHTMLTextBox'a geri geçirin ve sayfa döngüsünü tavanlayın- Markdown devam token'larını yalnızca
DrawMarkdownTextBox'a ya daDrawMarkdownText'e geçirin - UTF-16 bayt buffer'larında asla bayt deseni aramayın; bütün code unit'ler üzerinde çalışın
HTML ve Markdown render'ı, dataset rapor dışa aktarımı ve layout engine'in geri kalanı, Delphi ve Free Pascal için PDF Library for Delphi'nin native Pascal kaynak kodunda gelir. Sürümler, platform desteği ve deneme indirmesi için PDFlibPas ürün sayfasına bakın