Přechod na SafeHtml

Školení, která pořádám

Ve většině moderních aplikací vzniká HTML kód převážně v šablonách. To má nespornou výhodu v tom, že ošetřování dat stačí vyřešit na jednom místě. Můžeme použít automatické escapování, kontextově citlivé escapování, můžeme úplně zakázat vypsat text bez jeho ošetření. I v těchto aplikacích může být ale šikovné si tu a tam vygenerovat HTML kód, který do šablony pošleme. Můžeme totiž využít plnou sílu programovacího jazyka – metoda může vracet HTML kód a potomek ji může přetížit a vrátit něco jiného. Samozřejmě si tyto metody vevnitř můžou také zavolat šablonu, ale jednak někdy šablonovací systém nemají po ruce a jednak to je trochu jako kanón na vrabce, když chtějí vrátit třeba jenom <em>text</em>.

Facebook

Facebook žádné šablony nepoužívá a když nějaká část programu chce vytvořit HTML, tak to udělá pomocí XHP. Vede to k trochu odlišnému způsobu programování – místo velké aplikace, která generuje datové struktury, a velkých šablon, které tyto struktury zpracovávají a které jsou od hlavního kódu obvykle poměrně daleko, je všechno pěkně pohromadě. XHP je obvykle malé a jednoduché, protože vytváří jen malou část výsledku s využitím dalších komponent, které taky vytváří XHP. Všechny výhody šablon týkající se ošetřování dat jsou přitom díky XHP zachovány.

Phabricator

Phabricator vznikl ve Facebooku a je napsán do značné míry podobně. Nepoužívá žádné šablony, HTML kód se vytváří lokálně s využitím síly programovacího jazyka. Problém je, že nemá k dispozici XHP, takže HTML kód se vracel jako ručně ošetřované řetězce. Vzhledem k tomu, že kód vytváří velmi zkušení programátoři a všechny změny prochází code review, tak v historii nedošlo k mnoha případům nedostatečného nebo nadbytečného ošetřování. S kódem se ale pracovalo poměrně nepohodlně především proto, že nebylo jasné, co vrací nebo přijímá správně ošetřený HTML kód a co vrací nebo přijímá čistý text, který je nutno ošetřit.

Navrhl a realizoval jsem proto změnu, po které se všechny řetězce považují za čistý text a pokud něco chce vytvořit ošetřený HTML kód, tak musí vrátit PhutilSafeHtml. Vtip je v tom, že tento objekt si nemůže vytvořit jen tak někdo, ale musí k tomu použít jednu ze dvou funkcí. Buď phutil_tag, která přijímá název značky, pole atributů a obsah, nebo hsprintf, která přijímá formátovací řetězec a libovolný počet hodnot, které se před dosazením ošetří. Obsah může být buď řetězec, který se automaticky ošetří, nebo PhutilSafeHtml, kterému se věří. hsprintf v prvním parametru přijímá jen konstantní řetězec, za což ručí lint rule.

Při pohledu zpět se funkce hsprintf ukázala jako zbytečná – někdy je sice jednodušší ji použít, ale zbytečně zmírňuje silné záruky, které dává funkce phutil_tag. Ta dovoluje vytvořit jen správně spárované HTML značky, navíc může kontrolovat např. i hodnotu atributu <a href="">, aby nezačínala třeba javascript:. hsprintf nic z toho dost dobře dělat nemůže. Úplně by stačila funkce vytvářející značku a funkce spojující kombinaci neošetřených řetězců a PhutilSafeHtml do jednoho PhutilSafeHtml. A možná ani ta ne, protože funkce jako obsah přijímá i pole hodnot, které zřetězí.

Když odhlédnu od množství nudné práce, kdy bylo potřeba všechny řetězce vytvářející HTML kód převést na jednu z těchto dvou funkcí, tak byla změna vlastně docela jednoduchá. Pokud někdo konzumoval čistý text a dostal PhutilSafeHtml, tak se zavolala jeho metoda __toString. Maximálně hrozilo, že pokud tento text poslal dál, tak se při příštím použití znovu ošetřil. Tyto případy se nám podařilo celkem rychle vychytat. Pokud někdo začal konzumovat PhutilSafeHtml a dostal řetězec, tak ho prostě poslal dál – všechny funkce považují řetězec za platný vstup, který se na PhutilSafeHtml ve finále převede jeho ošetřením. Díky tomu není potřeba nikde v kódu ručně ošetřovat jakákoliv data.

Proč mi změna přišla vlastně docela jednoduchá, i když se týkala desítek tisíc řádek kódu? Hlavní důvod je ten, že celý kód máme pod kontrolou – pokud se rozhodneme změnit API nějaké metody, tak můžeme změnit i všechna její volání.

Closure Library

Obdobnou změnu nyní pomáhám realizovat i pro Closure Library. Closure Tools sice obsahuje vynikající šablony Closure Templates (alias Soy), knihovna na nich ale nezávisí, takže je nemůže používat.

Snažíme se odstranit všechna místa, která přiřazují řetězec do innerHTML, a nahradit je voláním funkce, která přijímá SafeHtml. Pokud budete chtít SafeHtml vytvořit z uživatelského vstupu, tak k tomu můžete použít funkci SafeHtml.from, která pro vás data ošetří. Pro vytvoření něčeho složitějšího jsem původně navrhl takovéto API:

var html = new SafeHtmlBuilder(TagName.A)
	.setAttribute('href', href)
	.addContent(text)
	.addContent(new SafeHtmlBuilder(TagName.BR).build())
	.build();

Byl to moc velký Javismus, takže jsme nakonec skončili u něčeho podobného jako ve Phabricatoru: SafeHtml.create('a', {'href': href}, [text, SafeHtml.create('br')]). Kromě toho vznikla ještě funkce SafeHtml.concat umožňující spojit více SafeHtml do jednoho.

Převod stávajícího API tak jednoduchý jako ve Phabricatoru bohužel nebude. Vezměme si třeba metodu goog.ui.tree.BaseNode#getHtml. Ta může být jak přetížená, tak ji může kdokoliv volat. Nemůžeme ji tedy změnit tak, aby prostě začala vracet SafeHtml, protože bychom všechny rozbili a navíc by naši potomci SafeHtml stejně nevraceli. Zvolili jsme tedy takovéto, poněkud krkolomné řešení:

Závěr

Zápis HTML jako řetězce sice napomohl jeho masivnímu rozšíření, ale ještě dnes přináší problémy i ve velkých společnostech. Pokud někde ve svém kódu uvidíte něco jako return "<br/>", tak je nejspíš vaše infrastruktura špatně a vyplatí se investovat energii do toho ji co nejdříve změnit. Čím později to uděláte, tím to bude bolestivější.

Jakub Vrána, Řešení problému, 12.3.2014, diskuse: 5 (nové: 0)

Diskuse

Radek Hřebeček:

Dost zajímavé děkuji, za hezký článek :)

paranoiq:

dík za článek :]

"Closure Tools sice obsahuje vynikající šablony Closure Templates (alias Soy), knihovna na nich ale nezávisí, takže je nemůže používat." - tohle vidím jako nepříjemný důsledek moderního trendu rozdrobovat dříve velké komplexní knihovny na co nejmenší *samostatné* komponenty. což je samo o sobě dobře, ale v některých případech to může jít proti poučce neduplikovat zbytečně kód. a týká se to každé větší knihovny, včetně třeba krájení Nette...

myslíš, že má tenhle problém nějaké řešení?

ikona Jakub Vrána OpenID:

Třeba Phabricator je rozdělen na Arcanist (rozhraní pro příkazový řádek), libphutil (společná knihovna) a samotný Phabricator. Někdy je otrava, že musím něco commitnout zvlášť do každého projektu, ale na druhou stranu jsem rád, že můžu použít třeba futures bez toho, abych musel záviset na celém Phabricatoru.

Takže řešení vidím asi v tom společné části vyčlenit do ještě menších knihoven a vyřešit jejich závislosti. Škoda, že Git nemá lepší submodules, to by řadu problémů vyřešilo.

Closure Library na Closure Templates nezávisí, protože by ji to zbytečně zvětšilo. A kvůli těm pár značkám, které Closure Library vytváří, se nevyplatí nahrávat celou sílu šablon.

Michal Raška:

Jakube za jakých podmínek lze užít PhutilSafeHtml v jiném projektu?

Díky

ikona Jakub Vrána OpenID:

libphutil včetně PhutilSafeHtml je k dispozici pod licencí Apache.

Diskuse je zrušena z důvodu spamu.

avatar © 2005-2024 Jakub Vrána. Publikované texty můžete přetiskovat pouze se svolením autora. Ukázky kódu smíte používat s uvedením autora a URL tohoto webu bez dalších omezení Creative Commons. Můžeme si tykat. Skripty předpokládají nastavení: magic_quotes_gpc=Off, magic_quotes_runtime=Off, error_reporting=E_ALL & ~E_NOTICE a očekávají předchozí zavolání mysql_set_charset. Skripty by měly být funkční v PHP >= 4.3 a PHP >= 5.0.