Latte verze 2.7: typy, našeptávání a sandbox režim

před 4 dny

David Grudl
Nette Core | 6944
+
+38
-

Měsíc po vydání Latte 2.6, které přineslo optional chaining a custom functions, je za dveřmi Latte 2.7, které je na novinky ještě zajímavější.

Typování

Každým dnem vyjde přelomová verze Latte pluginu, za kterou patří neskonalé uznání @mesour. Klíčová vlastnost je napovídání v šablonách stejně jako když píšete PHP kód. Aby napovídání mohlo fungovat, musí plugin vědět, co je ve které proměnné za typ.

U standardních proměnných, jak je třeba $user nebo $presenter, to plugin ví, o těch ostatních mu to musíme říct. A na to existuje několik způsobů.

1) Jednotlivě {varType type $param}

Typy jednotlivých parametrů (proměnných), které vstupují do šablony, můžete sdělit makrem {varType}. Takže třeba {varType ?Model\Article $article}.

2) Souhrnně {templateType MyTemplate} a {templatePrint}

Vytvoříte třídu, jejíž property budou odpovídat proměnným v šabloně, viz příklady.

class MyTemplate
{
	public \App\Module\Wiki\Presenters\ViewPresenter $presenter;
	public string $mediaPath;
	public array $langs;
	public string $lang;
	public \App\Module\Wiki\Model\Page $page;
	public array $sideBar;
}

Tato třída nemusí od ničeho dědit a představuje jakousi dokumentaci šablony. Kromě proměnných lze definovat i funkce, které potom budou v šabloně fungovat jako custom funkce.

A co dělá makro {templatePrint}? To se pokusí kód třídy vygenerovat, aby vám její psaní co nejvíc zjednodušilo.

3) Při vytváření proměnné {var type $x = ...} a {default type $x = ...}

Při implementaci této feature jsem narazil na historický relikt, a sice že tyto dvě makra podporují zápis proměnné bez $, takže funguje i {var x => 10}, což pak komplikuje parsování, protože nemusí být zcela jasné, jestli x je typ nebo proměnná. Z toho důvodu je tento historický zápis od Latte 2.7 označený jako deprecated. Zatím tedy funguje i nadále, ale vyhazuje chybovou hlášku.

Jednoduchý způsob jak prohledat své šablony, jestli tam tento starý zápis nemáte, je hledat regulární výrazy /\{var [^$]/ a /\{var [^}]*=>/.

Filter batch()

Na fóru se objevil požadavek na filtr, který by zjednodušil výpis tabulek z lineárních dat:

{var $items = ['a', 'b', 'c', 'd', 'e', 'f', 'g']}
<table>
{foreach ($items|batch:3:'No item') as $row}
    <tr>
        {foreach $row as $column}
            <td>{$column}</td>
        {/foreach}
    </tr>
{/foreach}
</table>

vygeneruje:

<table>
    <tr>
        <td>a</td>
        <td>b</td>
        <td>c</td>
    </tr>
    <tr>
        <td>d</td>
        <td>e</td>
        <td>f</td>
    </tr>
    <tr>
        <td>g</td>
        <td>No item</td>
        <td>No item</td>
    </tr>
</table>

Protože by se to mohlo hodit i více programátorům, přidal jsem ho přímo do Latte.

Otázkou zůstává, zda by nebylo lepší jej implementovat formou custom funkce:

{foreach batch($items, 3, 'No item') as $row}

Sandbox režim

Doslechl jsem se, že někteří uživatelé potřebují sandbox režim, kde lze omezit, co všechno může autor šablony dělat.

Sám jsem tomu nepřikládal nikdy důležitost, protože vycházím z toho, že při používání dependency injection může autor šablony dělat jen to, co mu dovolí objekty, které do ní programátor pošle. Pokud tedy hrozí nebezpeční, že by se koder přes $presenter->getContext()->getService('database')->query(...) dostával tam, kam nemá, jednoduše smažu proměnnou $presenter ze šablony. Ona je tam předaná jen pro pohodlí a vůbec tam být nemusí. Respektive žádná z automaticky předávaných proměnných tam být nemusí.

Faktem ale je, že lze těžko zamezit volání delete() nad výsledkem z Nette Database Explorer. Nebo přístupu k monostate třídám, jako je dibi::query(...). A nebo všem systémovým PHP funkcím. Tak jsem oprášil neuvěřitelné 4 roky starý commit a sandbox režim mergnul.

Přístup řídí objekt implementující Latte\Policy:

interface Policy
{
	function isMacroAllowed(string $macro): bool;

	function isFilterAllowed(string $filter): bool;

	function isFunctionAllowed(string $function): bool;

	function isMethodAllowed(string $class, string $method): bool;

	function isPropertyAllowed(string $class, string $property): bool;
}

jeho implementace Latte\Sandbox\Policy má v základu všechno zakázané a jednotlivé věci je potřeba povolit:

$policy = new Latte\Sandbox\Policy;
$latte->setPolicy($policy);

$policy->allowMacros(['=', 'foreach', 'block']);  // povolí {=$var} a tedy i {$var}, {foreach}, {block}
$policy->allowFilters($policy::ALL); // povolí všechny |filtry
$policy->allowFunctions(['trim', 'explode']); // povolí tyto globální funkce (a potažmo custom funkce)
$policy->allowMethods(DateTime::class, ['format']); // povolí volat tyto metody nad objekty DateTime
$policy->allowProperties(Nette\Database\Table\ActiveRow::class, $policy::ALL); // povolí přístup ke všem properties

Bezpečnostní pravidla se uplatní pro šablony, které se načtou novým makrem {sandbox untrustworthy.latte} (na rozdíl od {include ...}).

Pokud se mají pravidla použít už na první renderovanou šablonu, je potřeba to sdělit skrze: $latte->setSandboxMode();.

Je toho ještě více…

…ale myslím, že prozatím stačí :-)

Latte 2.7 je nyní určeno čistě pro testování. Vůbec si nejsem jistý, jestli API pro sandbox je dobře použitelné. A bez zpětné vazby na to nikdy nepřijdu, protože sandboxovat nepotřebuju. Netuším, jestli práce s typy přinese kýžené pohodlí, nebo to bude opruz. Tohle všechno je potřeba vyzkoušet a doladit.

Až vznikne pocit, že všechno funguje jak má (nebo naopak něco bude lepší ještě odložit), vyjde ostrá verze.