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).
<?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> <?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ápis | Vý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í |
| EMPTY | prázdný element, např. <br/> |
| ANY | cokoli (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ápis | Význam |
|---|---|
| CDATA | libovolný text |
| ID | jedinečný identifikátor v rámci dokumentu (platné jméno XML, nesmí začínat číslicí) |
| IDREF / IDREFS | odkaz na existující ID / více odkazů oddělených mezerou |
| NMTOKEN | jedno „slovo“ bez mezer |
| (a|b|c) | výčet povolených hodnot |
| #REQUIRED | atribut je povinný |
| #IMPLIED | atribut 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
, které XML samo nezná:
<!DOCTYPE dopis [
<!ENTITY firma "XML Servis s.r.o.">
<!ENTITY nbsp " ">
]>
<dopis>Dodavatel: &firma;, tel.: 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.
<!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
<!ELEMENT katalog (kniha+)>
<!ELEMENT kniha (nazev,cena)>
<!ATTLIST kniha id ID #REQUIRED>
<!ELEMENT nazev (#PCDATA)>
<!ELEMENT cena (#PCDATA)> <!DOCTYPE katalog SYSTEM "katalog.dtd">
<katalog><kniha id="k1"><nazev>XML</nazev><cena>199</cena></kniha></katalog> 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?
<!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.