Pular para conteúdo

Shortcodes

Shortcodes sao tags de conteudo dinamico processadas em campos de texto do Moodle. Usam a sintaxe [middag type="xyz" attr="val"] e sao substituidos por HTML em tempo de request.

Quando um administrador insere [middag type="widget" id="5"] na descricao de um curso, o framework localiza o handler registrado para widget e injeta o HTML retornado no lugar da tag.


Exemplo rapido

Registrar um shortcode e implementar o handler:

namespace local_meuplugin\extensions\catalogo;

use local_middag\facade\shortcode_manager;

class catalogo_extension extends \local_middag\base\extension
{
    public function boot(): void
    {
        shortcode_manager::register('product_card', function (array $attrs): string {
            $id = (int) ($attrs['id'] ?? 0);
            return "<span>Produto #{$id}</span>";
        });
    }
}

Uso no editor Moodle:

[middag type="product_card" id="42"]

Resultado renderizado:

<span>Produto #42</span>

Como funciona

  1. Extensions registram shortcodes via shortcode_manager::register(tag, callback) durante boot().
  2. O framework integra o processamento como Moodle text filter.
  3. Quando o Moodle renderiza um campo de texto, o filter faz early-return se [middag nao esta presente (otimizacao por string check antes de regex).
  4. Cada tag encontrada e parseada, os atributos sao extraidos, e o callback registrado e invocado.
  5. O HTML retornado substitui a tag original no texto.

Referencia da API

shortcode_manager (facade)

Metodo Descricao
register(string $tag, callable $callback) Registra um handler para a tag
render(string $text) Processa todas as tags [middag ...] no texto
has(string $tag) Verifica se uma tag esta registrada

Callback do handler

O callback recebe um array associativo com os atributos parseados e deve retornar uma string HTML:

function (array $attrs): string

Atributos sao parseados a partir da sintaxe key="value" ou key='value'. Exemplo:

[middag type="banner" title="Oferta" color="red"]

Gera:

['type' => 'banner', 'title' => 'Oferta', 'color' => 'red']

O atributo type identifica qual handler sera invocado. Os demais atributos sao livres.


Sanitizacao e #[trusted_output]

Por padrao, o output de todo shortcode e sanitizado via clean_text() antes de ser injetado no HTML. Isso remove tags potencialmente perigosas.

Handlers que precisam retornar HTML estruturado (divs, classes CSS, atributos data-*) devem declarar #[trusted_output] para desabilitar a sanitizacao automatica.

Handler como callable class com trusted_output

namespace local_meuplugin\extensions\catalogo;

use local_middag\framework\contract\attributes\trusted_output;

#[trusted_output]
class product_widget_shortcode
{
    public function __invoke(array $attrs): string
    {
        $id = (int) ($attrs['id'] ?? 0);
        $product = $this->load_product($id);

        return '<div class="product-widget" data-id="' . $id . '">'
             . '<h3>' . s($product->name) . '</h3>'
             . '<p>' . s($product->description) . '</p>'
             . '</div>';
    }

    private function load_product(int $id): object
    {
        // Consulta ao repositorio...
    }
}

Registro no boot():

shortcode_manager::register('product_widget', new product_widget_shortcode());

Responsabilidade de seguranca

Ao usar #[trusted_output], voce assume total responsabilidade pela seguranca do output. Use s() ou format_text() para escapar dados de usuario. Nunca injete dados nao-tratados no HTML.

O atributo #[trusted_output] pode ser aplicado em classes, funcoes ou metodos.


Handler como metodo de classe

Para handlers que precisam de dependencias injetadas, use um metodo de classe:

namespace local_meuplugin\extensions\catalogo;

use local_middag\framework\contract\attributes\trusted_output;

class catalogo_shortcodes
{
    public function __construct(
        private readonly catalogo_repository_interface $catalogo,
    ) {}

    #[trusted_output]
    public function render_banner(array $attrs): string
    {
        $slug = $attrs['slug'] ?? '';
        $banner = $this->catalogo->get_banner($slug);

        return '<div class="banner">' . s($banner->html) . '</div>';
    }
}

Registro no boot():

$shortcodes = new catalogo_shortcodes($this->container->get(catalogo_repository_interface::class));
shortcode_manager::register('banner', [$shortcodes, 'render_banner']);

Tratamento de falhas

Cenario Comportamento
Tag nao registrada Ignorada silenciosamente (fail safe). Em DEBUG_DEVELOPER, exibe warning.
Shortcode malformado Ignorado (regex nao faz match)
Excecao no handler Propaga no fluxo do text filter (o handler compoe o output final)
Texto sem [middag Early-return imediato, sem custo de regex

Patterns

Data lookup shortcode

Shortcode que busca um registro e exibe dados formatados:

shortcode_manager::register('user_count', function (array $attrs): string {
    global $DB;
    $count = $DB->count_records('user', ['confirmed' => 1]);
    return (string) $count;
});

Uso: Temos [middag type="user_count"] usuarios confirmados.

Widget embedding

Shortcode que renderiza um widget complexo com markup HTML. Requer #[trusted_output]:

#[trusted_output]
class chart_shortcode
{
    public function __invoke(array $attrs): string
    {
        $metric = s($attrs['metric'] ?? 'enrollments');
        $period = s($attrs['period'] ?? '30d');

        return '<div class="middag-chart" '
             . 'data-metric="' . $metric . '" '
             . 'data-period="' . $period . '">'
             . '</div>';
    }
}

Shortcode com conteudo condicional (segments)

Combine shortcodes com o sistema de segmentos para conteudo direcionado:

shortcode_manager::register('if_segment', function (array $attrs): string {
    $segment_id = (int) ($attrs['segment_id'] ?? 0);
    $content = $attrs['content'] ?? '';

    $segments = \local_middag\facade\segments::get_subquery($segment_id);
    global $DB, $USER;

    if ($DB->record_exists_sql(
        "SELECT 1 FROM ({$segments->sql}) sub WHERE sub.userid = ?",
        array_merge($segments->params, [$USER->id])
    )) {
        return s($content);
    }

    return '';
});

Anti-patterns

Computacao pesada no handler

Shortcodes sao processados em cada renderizacao de texto. Operacoes custosas degradam a performance da pagina inteira:

// ERRADO: query pesada executada a cada render
shortcode_manager::register('report', function (array $attrs): string {
    // Agregacao de milhares de registros a cada pageview...
    $data = $DB->get_records_sql("SELECT ... GROUP BY ... HAVING ...");
    return format_report($data);
});

Use cache ou pre-calcule os dados fora do handler.

Output nao-sanitizado sem trusted_output

Sem #[trusted_output], o output passa por clean_text(). Nao tente injetar HTML estruturado sem a anotacao — as tags serao removidas:

// ERRADO: HTML sera limpo pelo sanitizador
shortcode_manager::register('card', function (array $attrs): string {
    return '<div class="card"><h3>Titulo</h3></div>'; // sera stripped
});

// CORRETO: declarar trusted_output na callable class

Dados de usuario sem escape em trusted_output

Com #[trusted_output], o framework confia no developer. Nunca injete dados crus:

// ERRADO: XSS vulneravel
#[trusted_output]
class vuln_shortcode
{
    public function __invoke(array $attrs): string
    {
        return '<div>' . $attrs['name'] . '</div>'; // XSS!
    }
}

// CORRETO: usar s() para escapar
return '<div>' . s($attrs['name']) . '</div>';

Referencias

  • ADR-704 -- Processamento de Shortcodes
  • Hooks -- sistema reativo (shortcodes nao sao hooks)
  • Modos de Integracao -- como registrar shortcodes de fora do framework