DTD: definice typu dokumentu

DTD (Document Type Definition) je nejstarší způsob, jak popsat povolenou strukturu XML dokumentu. Je přímo součástí specifikace XML, a proto se s ní pořád setkáš ve starších formátech, v deklaracích DOCTYPE a při definici entit.

Autor: Aktualizováno

Deklarace DOCTYPE

DTD se k dokumentu připojuje deklarací <!DOCTYPE>. Ta patří do prologu: stojí za XML deklarací a před kořenovým elementem. Za klíčovým slovem následuje jméno kořenového elementu a pak buď pravidla přímo v hranatých závorkách (interní DTD), nebo odkaz na samostatný soubor (externí DTD).

Interní DTD
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE adresar [
  <!ELEMENT adresar (kontakt+)>
  <!ELEMENT kontakt (jmeno, email*, telefon?)>
  <!ELEMENT jmeno (#PCDATA)>
  <!ELEMENT email (#PCDATA)>
  <!ELEMENT telefon (#PCDATA)>
  <!ATTLIST kontakt id ID #REQUIRED>
  <!ATTLIST email typ (pracovni|soukromy) "soukromy">
]>
<adresar>
  <kontakt id="k1">
    <jmeno>Jana Nováková</jmeno>
    <email typ="pracovni">jana@example.cz</email>
  </kontakt>
</adresar>
Externí DTD
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE adresar SYSTEM "adresar.dtd">
<adresar>
  …
</adresar>

Externí DTD může mít i veřejný identifikátor (PUBLIC "-//…//EN" "url"), jak to znáš ze starších verzí HTML. HTML5 z toho zachovalo jen zkrácené <!DOCTYPE html>, které už na žádné DTD neodkazuje.

Deklarace elementů

<!ELEMENT jméno obsah> určuje, co smí element obsahovat. Model obsahu se skládá z těchto zápisů:

ZápisVýznam
(a, b, c)potomci a, b, c přesně v tomto pořadí
(a | b)buď a, nebo b
a?nula nebo jeden výskyt
a*libovolný počet výskytů včetně nuly
a+jeden a více výskytů
(#PCDATA)jen text
(#PCDATA | b | i)*smíšený obsah: text a elementy b, i v libovolném pořadí
EMPTYprázdný element, např. <br/>
ANYcokoli (používá se jen výjimečně)

V ukázce výše musí mít každý kontakt jméno, pak libovolný počet e-mailů a nakonec nejvýš jeden telefon.

Deklarace atributů

<!ATTLIST element atribut typ výchozí> popisuje atributy. V jedné deklaraci jich může být i víc.

ZápisVýznam
CDATAlibovolný text
IDjedinečný identifikátor v rámci dokumentu (platné jméno XML, nesmí začínat číslicí)
IDREF / IDREFSodkaz na existující ID / více odkazů oddělených mezerou
NMTOKENjedno „slovo“ bez mezer
(a|b|c)výčet povolených hodnot
#REQUIREDatribut je povinný
#IMPLIEDatribut je nepovinný a nemá výchozí hodnotu
#FIXED "x"atribut má vždy hodnotu x
"x"výchozí hodnota, pokud atribut chybí

Entity

DTD umí definovat vlastní entity — pojmenované zkratky, které parser při čtení nahradí jejich hodnotou. Díky tomu lze v XML používat i entity známé z HTML, jako &nbsp;, které XML samo nezná:

<!DOCTYPE dopis [
  <!ENTITY firma "XML Servis s.r.o.">
  <!ENTITY nbsp "&#160;">
]>
<dopis>Dodavatel: &firma;, tel.:&nbsp;123</dopis>

Kromě těchto obecných entit existují parametrické entity (zapisují se se znakem %), které se používají jen uvnitř DTD pro opakující se části pravidel.

Bezpečnost: XXE a „billion laughs“

Entity jsou i nejčastějším zdrojem bezpečnostních chyb při zpracování XML:

  • XXE (XML External Entity) — externí entita může odkazovat na soubor nebo URL. Pokud aplikace zpracuje podvržený dokument s povoleným načítáním externích entit, může útočníkovi vrátit obsah souborů ze serveru nebo za něj posílat požadavky do vnitřní sítě.
  • Billion laughs — entity definované pomocí dalších entit se při rozbalení exponenciálně násobí. Pár kilobajtů DTD tak může vyčerpat paměť serveru.

Doporučení: u XML z nedůvěryhodných zdrojů vypni v parseru zpracování DTD a externích entit, pokud je výslovně nepotřebuješ. Moderní verze knihoven to mají často vypnuté ve výchozím nastavení, ale spoléhat se na to nevyplatí — ověř to v dokumentaci konkrétního parseru.

Ukázky útoků a konkrétní nastavení parserů v PHP, .NET, Javě, Pythonu a Node.js najdeš v kapitole XXE: útok přes externí entity. DTD samotné si můžeš ověřit ve validátoru na záložce Schéma.

Omezení DTD a srovnání s XSD

  • Omezené typy — DTD má typy atributů, například ID a NMTOKEN, ale ne číselné ani datumové typy pro text elementů.
  • Nepodporuje jmenné prostory — prefix je pro DTD jen součást jména, takže dokument s jiným prefixem neprojde.
  • Vlastní syntaxe — DTD není XML, takže se nedá zpracovat běžnými XML nástroji.
  • Bez přímého zápisu min/max — DTD používá ?, * a +. Omezený počet lze rozepsat kombinací povinných a nepovinných prvků, ale u větších mezí je to nepřehledné.

Pro nové formáty proto použij XML Schema (XSD). DTD má dnes smysl hlavně tam, kde ho vyžaduje existující formát, nebo pro definici entit.

Parametrické entity a podmíněné sekce

Parametrická entita se deklaruje znakem % a odkazuje zápisem %jmeno;. Slouží k modularizaci DTD; běžná entita s ampersandem naopak vstupuje do obsahu dokumentu. Následující příklad patří do externího souboru katalog.dtd.

Externí DTD
<!ENTITY % obsah "(nazev, cena)">
<!ELEMENT kniha %obsah;>
<!ELEMENT nazev (#PCDATA)>
<!ELEMENT cena (#PCDATA)>
<![INCLUDE[
<!ATTLIST kniha id ID #REQUIRED>
]]>

INCLUDE zahrne deklarace, IGNORE je přeskočí. Podmíněné sekce nejsou dovoleny v interní podmnožině DOCTYPE. Interní podmnožina má také přísnější pravidla použití parametrických entit uvnitř deklarací. Nezaměňuj je s atributy ani makry aplikace; načítání externí DTD musí být omezené na známé zdroje.

Externí DTD: soubory a pozitivní/negativní test

katalog.dtd — lokální soubor
<!ELEMENT katalog (kniha+)>
<!ELEMENT kniha (nazev,cena)>
<!ATTLIST kniha id ID #REQUIRED>
<!ELEMENT nazev (#PCDATA)>
<!ELEMENT cena (#PCDATA)>
katalog.xml — DTD i dokument ve stejné složce
<!DOCTYPE katalog SYSTEM "katalog.dtd">
<katalog><kniha id="k1"><nazev>XML</nazev><cena>199</cena></kniha></katalog>
Spuštění lokální validace
xmllint --nonet --noout --valid katalog.xml

U správného dokumentu příkaz nevypíše validační chybu a vrátí úspěšný návratový kód. Odeber id nebo zaměň pořadí cena a nazev: DTD validace selže, přestože XML stále může být dobře utvořené. Cena abc projde, protože DTD textu neurčuje číselný typ. Pro takové pravidlo potřebuješ XSD, RELAX NG s typy nebo aplikační kontrolu.

DTD se načítá pouze v důvěryhodném lokálním testu. --nonet zakazuje síťové načítání podle možností této knihovny; samo neomezuje všechny lokální soubory. Pro nedůvěryhodné vstupy nepovoluj obecně externí DTD ani entity. Přesměrování odkazů na předem určené místní soubory vysvětlují XML Catalogs.

Lze v DTD předepsat dvě až pět položek?

Fragment DTD — dvě povinné položky a až tři další
<!ELEMENT kosik (polozka,polozka,(polozka,(polozka,polozka?)?)?)>
<!ELEMENT polozka (#PCDATA)>

Ano. První dvě položky jsou povinné; každá další se může objevit až po předchozí. Jeden nebo šest výskytů validací neprojde. Pro často měněné meze je přehlednější XSD s minOccurs a maxOccurs. Pravidla skupin a opakování popisuje XML 1.0, oddíl 3.2.1.