Latte Templating Engine

repository·master·Indexed 22 days ago

https://github.com/nette/latte

A secure, high-performance templating engine for PHP designed to protect against vulnerabilities like XSS. It features context-sensitive escaping, a compilation pipeline that transforms templates into PHP classes, and a sandbox for secure template execution. The documentation covers syntax, tags, filters, template inheritance via blocks and layers, and internal details on the compilation process and cache signatures.

Tokens
6.9K
Snippets
2
Records
42
Agent score
72%

What's inside Latte

  1. Use ContentType and contextual filters to control escaping

    master

    Latte uses ContentType to track the type of content being processed. ContentType is a final class containing string constants:

    • Html='html'
    • Text='text'
    • JavaScript
    • Css
    • Xml
    • ICal

    Contextual Filters

    A contextual filter can interact with the FilterInfo object to declare the nature of its output. This is primarily used to:

    • Disable auto-escaping: A filter can set $info->contentType = ContentType::Html to signal that its output is already valid HTML, preventing Latte from escaping it again.
    • Validate input: Filters can use FilterInfo::validate to assert what input type they accept.

    Note: Enforcement of content types for filters applied to {block} is performed at runtime, not compile time. A filter might be accepted when the incoming content type is text but will throw an error if the content type does not match its requirements during execution.

  2. Understand how blocks and layers work in Latte

    master

    Latte organizes blocks into three primary runtime layers. When you look up a block by name, Latte resolves it by checking the local layer first, then falling back to the top layer: blocks[LayerLocal][$name] ?? blocks[LayerTop][$name].

    The Layers

    • LayerTop (integer 0): The default layer for blocks defined at the template top level or within an {embed}. Most blocks reside here.
    • LayerLocal ('local'): Targeted only when using the local keyword (e.g., {block local name} or {define local name}).
    • LayerSnippet ('snippet'): Used for snippet-based rendering.

    Block vs. Define

    • {block}: Registers a block and also renders it in place.
    • {define}: Only registers the block without rendering it.
  3. Block availability when using {include ... from}

    master

    When you use {include X from 'target'} (e.g., to render a specific block from another template), Latte executes target->render('X'). This renders only that specific block, not the template's main() method. This has significant implications for what is available during that render:

    • {import} requirements: For an imported block to be available via {include ... from}, the {import} statement must be located in the template head. If any content exists before the {import}, it is pushed into main(), which is not executed during a single-block render, causing the include to fail.
    • Intermediate local blocks: A block marked as local in an intermediate template in an inheritance chain is not available via lookup from a child. Only LayerTop merges up. The lookup always runs on the topmost parent, so only the topmost parent's LayerLocal is checked.
  4. Understand the Latte compilation pipeline

    master

    Latte transforms template source code into a PHP class through several stages:

    1. Source
    2. TemplateLexer: Tokenizes the source.
    3. TemplateParser (including TemplateParserHtml and TagParser): Converts tokens into an Abstract Syntax Tree (AST).
    4. AST Passes: The AST is traversed by various passes (e.g., for security or optimization).
    5. TemplateGenerator: Converts the processed AST into PHP code.
    6. PHP Class: The final output is a compiled PHP class.

    Note that in-tag content is tokenized by a separate TagLexer, and TagParser is a generated LALR parser driven by TagParserData.

  5. How the spaceless tag and filter work

    master

    Latte provides three ways to trigger whitespace minification, which are powered by the Essential/WhitespaceMinifier stateful streaming tokenizer:

    1. {spaceless} tag: Acts as an ob_start handler. It minifies output as it flows through the buffer rather than waiting for the entire document to complete.
    2. n:spaceless tag: (Namespace variant).
    3. |spaceless filter: Performs a one-shot minify() on an already-complete string.

    Minification Semantics by Content Type:

    • HTML: Collapses inter-tag whitespace and drops it entirely around whitespace-insensitive elements (e.g., br, hr, option, link). Note that pre and textarea are treated as raw text and their contents are protected verbatim.
    • XML: Treats all tags as whitespace-insensitive.
    • text/js/css/ical: Collapses spaces and tabs but preserves newlines. In /attr (attribute) contexts, newlines are also collapsed to a single space.
    • Other: Any non-HTML/XML content type follows the text path.
  6. How nested {spaceless} tags are handled

    master

    When using the {spaceless} tag, nesting multiple tags does not create multiple output buffers. Instead, the minifier uses a static depth counter to manage the process-global output buffering:

    • The start() method increments a static $depth counter.
    • Only the outermost call triggers the actual ob_start.
    • The end() method flushes the buffer only when the depth returns to 0.

    This ensures that nested {spaceless} blocks are treated as a single minification scope.

  7. How the Sandbox pass works

    master

    The SandboxExtension uses a Policy interface to restrict what can be executed within a template. It operates by registering a pass that runs before: '*' (before all other passes) and traverses the AST during the leave phase.

    Key behaviors include:

    • Static Restrictions: It statically forbids $this, variable-variables, |noescape, and new.
    • Static Checks: It checks function and filter names against the Policy interface (methods: isTagAllowed, isFilterAllowed, isFunctionAllowed, isMethodAllowed, isPropertyAllowed). Even allowed functions/filters have their arguments rewritten through a runtime args() guard.
    • Runtime Enforcement: Property/method fetches and calls are replaced with Sandbox\Nodes\* wrappers. These emit runtime RuntimeChecker calls (callMethod, prop, call) to enforce access rules at render time rather than compile time.
    • Accessing the Checker: The runtime checker is exposed as the sandbox provider at beforeRender.
  8. How dynamic attributes handle escaping

    master

    When an entire HTML attribute is generated from an expression (using Html\ExpressionAttributeNode), it bypasses the standard Escaper state machine. Instead, it uses specific Runtime\HtmlHelpers::format*Attribute methods determined at compile time.

    The attribute type is determined by the attribute name (via classifyAttributeType) or forced by modifiers:

    • Attribute Modifiers:
      • |toggle: Forces a specific attribute type.
    • JSON: The |json modifier can be used, but it must be the last modifier in the chain.

    These formatters (a "third layer" of escaping) handle their own logic, such as escapeAttr or JSON smart-quoting, to ensure that values like false in a hidden attribute are handled correctly according to HTML specifications.

  9. Use `AuxiliaryNode` for opaque code generation

    master

    AuxiliaryNode (available in area and expression variants) allows you to carry a closure as its print method. The body of this closure is opaque to compiler passes, meaning passes cannot see or rewrite the PHP it emits.

    Security Warning: Because the closure body is invisible to security passes, you must never bake user-provided expressions directly into the closure. Instead, pass any user-controlled nodes into the AuxiliaryNode via its separate $nodes list, which remains traversable via &getIterator.

  10. Understand the implicit `__toString` coercion gap in the sandbox

    master

    The Latte sandbox does not guard against implicit object-to-string coercion. While an explicit call like {$obj->__toString()} is blocked, the following will still trigger __toString() even if the method is not allowed by the Policy:

    • Direct interpolation: {$obj}
    • Concatenation
    • String interpolation
    • (string) casts
    • Filters
    • Loose comparisons

    This is a deliberate design decision. The security responsibility lies with the host: do not expose objects with dangerous __toString implementations to the template engine.

  11. Use {include parent} and {include this} in blocks

    master

    Latte provides special tags to reference the current or parent block context:

    Static Blocks

    Inside a block with a static name, {include parent} and {include this} resolve the name at compile time based on the closest enclosing block.

    Dynamically Named Blocks

    Inside a block with a dynamic name, the compiler cannot resolve the name at compile time. Instead, the runtime retrieves the name from Template::$renderingBlocks (a stack of names currently being rendered).

    Warning on Nesting: If you nest {include parent} inside a dynamic block that is itself inside a static block, the tag will refer to the dynamic block's runtime name. It will not automatically skip the dynamic block to find the outer static block unless the tag includes a from clause (which requires a compile-time name).

  12. Configure Extension and Pass ordering

    master

    Latte uses different ordering mechanisms for Passes and Tags:

    Passes

    Passes are topologically sorted across all extensions using Helpers::sortBeforeAfter. You can use the Extension::order($subject, before:, after:) method to return a marker. Using '*' as a value for before or after means the pass will be positioned relative to all other passes (e.g., before: '*' runs before every other pass).

    Tags

    Tags follow a last-registered-wins rule. TemplateParser::addTags overwrites existing entries for a tag name. If two extensions define the same tag, the one registered later will take effect. Note that before/after markers do not affect tag registration priority; they are dropped for {tag} parsers.