Files
emcp-tools-mapping/docs/12-WIDGET-BUILDER-BLUEPRINT.md
T
Claude Code 58acdd71e3 docs: doc 12 blueprint Widget Builder (spec->widget Elementor PHP)
Fundamentado na API publica real do Elementor (Widget_Base, Controls_Manager,
Widgets_Manager, Elements_Manager, widget core Heading como exemplo) mais os
padroes de seguranca ja confirmados em codigo real Free (EMCP_Tools_Widget_Store/
Widget_Loader do doc06 - manifest-only, tamper guard sha256, path containment,
runtime_validate). O compilador spec->PHP em si (EMCP_Tools_Widget_Generator) e
a camada de abilities MCP continuam ausentes do build Free - marcado [REAL] vs
[DESENHO PROPRIO] em cada afirmacao do documento.

Recomenda 'zero tool de activacao via MCP' para set-widget-status, seguindo a
mesma filosofia ja confirmada no Themer PHP e nos PHP Snippets (aprovacao
humana obrigatoria antes de codigo gerado por IA correr em producao).

Actualiza INDEX.md com a 13a entrada e totais revistos.
2026-08-19 07:42:21 +01:00

45 KiB

12 — Widget Builder: blueprint de abilities MCP (desenho próprio, não é auditoria EMCP Pro)

Fonte primária: leitura directa (19-08-2026) do Elementor 4.2.2 (build Free) instalado em emanuelalmeida.pt (/home/ealmeida/emanuelalmeida.pt/wp-content/plugins/elementor/) — a API pública real com que qualquer gerador de widgets tem de trabalhar. Cruzado com docs/06-SANDBOX-CUSTOM-CODE.md §3.9-3.10 (código real, Free, já lido nesta série: EMCP_Tools_Widget_Store e EMCP_Tools_Widget_Loader) e docs/04-THEMER.md §11 (o padrão de aprovação humana do Themer PHP). Este documento NÃO lê nem audita o compilador spec→PHP nem a camada de abilities MCP do Widget Builder Pro — ambos estão fisicamente ausentes da árvore Free instalada (confirmado em docs/06-SANDBOX-CUSTOM-CODE.md §3.9, citação directa: "Também confirmado ausente: includes/class-widget-generator.php (o compilador spec→PHP) e includes/class-block-store.php"; e includes/abilities/class-widget-builder-abilities.php também ausente da listagem de includes/abilities/).

0. Panorama — porque este documento é diferente dos outros 11 e o que fundamenta cada peça

A captura de ecrã da UI de administração do EMCP Tools (secção Widget Builder, marcada "Requires EMCP Pro") mostra 8 tools MCP, todas prefixadas emcp-tools/: list-control-types, validate-widget-spec, create-custom-widget, update-custom-widget, get-custom-widget, list-custom-widgets, set-widget-status, delete-custom-widget. Estes 8 nomes são usados apenas como especificação da API-alvo (nomenclatura/âmbito de cada tool) — não como fonte de implementação. Não há código Pro nesta árvore para ler, logo não há descrições de input schema, mensagens de erro, nem lógica interna do EMCP Pro para citar.

Em vez disso, este documento responde à pergunta inversa dos docs 01-10: dado o que o Elementor em si expõe como API pública de widgets (\Elementor\Widget_Base, Controls_Manager, Widgets_Manager, Elements_Manager) e dado o modelo de segurança de código-gerado-por-IA já confirmado e testado noutras partes desta mesma árvore Free (Sandbox de PHP Snippets, Themer PHP, e a camada de armazenamento EMCP_Tools_Widget_Store que existe mesmo sem a camada de abilities), como desenharíamos do zero um sistema "Widget Builder"?

Três fontes reais fundamentam cada peça deste blueprint, e cada secção abaixo identifica explicitamente qual delas usa:

  1. API pública real do Elementor Free (lida nesta sessão): includes/base/widget-base.php (Widget_Base, classe abstracta que qualquer widget custom tem de estender), includes/managers/controls.php (Controls_Manager, catálogo real de tipos de controlo), includes/managers/widgets.php (Widgets_Manager, registo/lookup de widgets), includes/managers/elements.php (Elements_Manager, categorias), e includes/widgets/heading.php (Widget_Heading, um widget core real como exemplo concreto de register_controls()/render()).
  2. Padrões de segurança já confirmados (código real, Free) em docs/06-SANDBOX-CUSTOM-CODE.md — nomeadamente EMCP_Tools_Widget_Store (§3.9, existe na build Free, 839 linhas lidas na íntegra) e EMCP_Tools_Widget_Loader (§3.10, idem), mais o validador de PHP partilhado (EMCP_Tools_PHP_Snippet_Validator, §3.3) e o padrão de aprovação humana do Themer PHP (docs/04-THEMER.md §11-§2, também código real).
  3. O catálogo de 8 nomes de tool da captura de ecrã — usado só como lista de âmbito/naming alvo (secção 7).

Convenção usada em todo o documento: cada afirmação é marcada [REAL] quando vem directamente do código lido (Elementor ou EMCP Free já documentado nesta série), ou [DESENHO PRÓPRIO] quando é uma proposta nossa sem equivalente confirmado no EMCP Pro. Onde uma peça combina as duas (ex.: reaproveitar uma classe real do EMCP Free para uma função nova), isso é dito explicitamente.

A distinção mais importante de todo o documento: a camada de armazenamento de um widget custom (EMCP_Tools_Widget_Store/EMCP_Tools_Widget_Loader) já existe e é real — só a camada que a alimenta (o compilador spec→PHP, EMCP_Tools_Widget_Generator) e a camada que a expõe a agentes de IA (EMCP_Tools_Widget_Builder_Abilities) é que estão ausentes e são, portanto, o verdadeiro objecto deste desenho próprio. As secções 6 e parte da 7 apoiam-se fortemente em código real já lido; as secções 1, 4 e 5 são as mais "desenho próprio" deste documento.


1. A "spec" de um widget — estrutura de dados proposta [DESENHO PRÓPRIO]

Um agente de IA nunca deveria escrever PHP cru para criar um widget — a citação já confirmada em docs/06-SANDBOX-CUSTOM-CODE.md §3.9 sobre o EMCP_Tools_Widget_Store real é explícita quanto a isto: "estes widgets são PHP compilado por este plugin a partir de uma spec fornecida por IA (a IA nunca escreve PHP cru)". Adoptamos o mesmo princípio de design aqui — a spec é a única superfície que um agente manipula; um compilador (secção 5) gera o PHP.

Proposta de schema JSON (nomes de campo nossos, [DESENHO PRÓPRIO]; os TIPOS de controlo válidos dentro de controls[].type são fundamentados em Controls_Manager::get_controls_names(), [REAL] — ver secção 3):

{
  "name": "testimonial_card",
  "title": "Testimonial Card",
  "icon": "eicon-testimonial",
  "category": "general",
  "keywords": ["testimonial", "quote", "review"],
  "controls": [
    {
      "id": "quote_text",
      "type": "textarea",
      "label": "Quote",
      "default": "Add your quote here",
      "section": "content",
      "tab": "content"
    },
    {
      "id": "author_name",
      "type": "text",
      "label": "Author",
      "default": "",
      "section": "content",
      "tab": "content"
    },
    {
      "id": "author_photo",
      "type": "media",
      "label": "Photo",
      "default": { "url": "" },
      "section": "content",
      "tab": "content"
    },
    {
      "id": "card_bg_color",
      "type": "color",
      "label": "Background",
      "default": "#ffffff",
      "section": "style",
      "tab": "style",
      "selectors": { "{{WRAPPER}} .emcpb-card": "background-color: {{VALUE}};" }
    }
  ],
  "sections": [
    { "id": "content", "label": "Content", "tab": "content" },
    { "id": "style", "label": "Card Style", "tab": "style" }
  ],
  "render_template": "<div class=\"emcpb-card\" style=\"background:{{card_bg_color}}\"><blockquote>{{quote_text}}</blockquote><cite>{{author_name}}</cite></div>",
  "css": ".emcpb-card{padding:24px;border-radius:8px}",
  "js": null
}

Campos e a sua justificação:

Campo Fundamentação Nota
name [REAL] — mapeado directamente para Widget_Base::get_name(), o identificador único que Widgets_Manager::register() usa como chave ($this->_widget_types[$widget_instance->get_name()] = $widget_instance, includes/managers/widgets.php:283) Tem de ser único no site (secção 4)
title/icon/category/keywords [REAL] — mapeiam 1:1 para get_title(), get_icon() (default 'eicon-apps'), get_categories() (default ['general']), get_keywords() (default []), todos métodos overridable confirmados em widget-base.php category é singular na nossa spec por simplicidade; Elementor aceita array (get_categories() devolve array) — o compilador (secção 5) gera sempre return [ $spec['category'] ];
controls[] [DESENHO PRÓPRIO] (a lista), type de cada item [REAL] (secção 3) Cada item vira uma chamada add_control()
controls[].section/sections[] [DESENHO PRÓPRIO], mapeado para start_controls_section()/end_controls_section() ([REAL], método herdado, usado exactamente assim em heading.php: $this->start_controls_section('section_title', ['label' => ...])) Agrupa controlos em secções colapsáveis no editor
controls[].tab [REAL] os valores válidos (Controls_Manager::TAB_CONTENT/TAB_STYLE/TAB_ADVANCED/etc., constantes confirmadas em controls.php) Default content se omitido
controls[].selectors [REAL] — mecanismo Elementor nativo de CSS live-preview, visto usado extensivamente em heading.php (ex. '{{WRAPPER}} .elementor-heading-title' => 'color: {{VALUE}};') Só faz sentido nos controlos de Style
render_template [DESENHO PRÓPRIO] — string com placeholders {{control_id}}, ver secção 5 Alternativa a fornecer PHP raw (que passaria pelo validador do sandbox, secção 6)
css/js [DESENHO PRÓPRIO], mas o mecanismo de assets opcionais por widget (ficheiro .css/.js ao lado do .php gerado) é [REAL] — confirmado em EMCP_Tools_Widget_Store (docs/06 §3.9: wp-content/emcp-sandbox/widgets/{id}/widget.php + style.css/script.js opcionais) Reaproveitado tal-qual, ver secção 6

2. Como um widget é normalmente registado — contrato real do Elementor [REAL]

Fundação obrigatória para qualquer gerador: um widget Elementor é uma instância de uma classe PHP que estende \Elementor\Widget_Base (includes/base/widget-base.php:29, abstract class Widget_Base extends Element_Base), registada via Widgets_Manager::register(Widget_Base $widget_instance) (includes/managers/widgets.php:261), chamada dentro do hook elementor/widgets/register (confirmado directamente: do_action('elementor/widgets/register', $this), includes/managers/widgets.php:159, substituiu o antigo elementor/widgets/widgets_registered deprecated desde a 3.5.0). Este é exactamente o mesmo hook que docs/04-THEMER.md §10 já confirmou ser usado pelo EMCP_Tools_Themer_Widgets (widgets dinâmicos do Themer) — require_once tardio da classe custom dentro deste hook, "quando \Elementor\Widget_Base está garantidamente carregado".

Contrato mínimo de métodos que uma classe de widget tem de implementar/pode sobrepor (confirmados por leitura directa de widget-base.php + o exemplo real Widget_Heading):

Método Obrigatório? Confirmado em
get_name(): string Sim (abstracto, herdado de Element_Base/Controls_Stack — não visível no corpo de Widget_Base, mas usado como chave única em register()) Widgets_Manager::register(), Widget_Heading::get_name() (return 'heading';)
get_title(): string Sim (idem) Widget_Heading::get_title()
get_icon(): string Não — default 'eicon-apps' widget-base.php (método com corpo default)
get_categories(): array Não — default ['general'] widget-base.php
get_keywords(): array Não — default [] widget-base.php
register_controls(): void Sim, protected Chamado internamente por Controls_Stack::init_controls(); ver Widget_Heading::register_controls() completo, usa start_controls_section()/add_control()/add_responsive_control()/add_group_control()/end_controls_section()
render(): void Sim, protected Widget_Heading::render(): lê $this->get_settings_for_display(), monta HTML, faz echo (com comentário PHPCS explícito de que a variável já está segura — wp_kses_post() aplicado antes)
content_template(): void Não Template Backbone.js para preview live no editor sem round-trip ao servidor — fora de âmbito de um gerador v1 (ver secção 8)

get_stack($with_common_controls = true) (widget-base.php, confirmado) é o método que força a resolução completa do stack de controlos de um widget — internamente chama init_controls() (que, em Widget_Base, reinicia a flag is_first_section e depois chama parent::init_controls(), o qual invoca register_controls()). Isto é directamente relevante para a secção 6: instanciar uma classe gerada e chamar get_stack() é a forma real de forçar register_controls() a correr e apanhar um erro de sintaxe/lógica ANTES do widget chegar ao editor — e é exactamente o que EMCP_Tools_Widget_Store::runtime_validate() já faz, confirmado em docs/06 §3.9.

Categorias (Elements_Manager::get_categories(), includes/managers/elements.php:121, init_categories() lido na íntegra) — as nativas confirmadas nesta versão: v4-elements ("Atomic Elements"), layout, basic, pro-elements, helloplus, general, link-in-bio, theme-elements, woocommerce-elements (+ atomic-form condicional ao experiment e_atomic_elements). Categorias custom registam-se via Elements_Manager::add_category($name, $properties) no hook elementor/elements/categories_registered — o mesmo hook que docs/04-THEMER.md §10 confirma ser usado pelo Themer para a sua categoria "EMCP Themer".


3. list-control-types — catálogo real de tipos de controlo [REAL] + parâmetros [misto]

A lista de tipos vem directamente das constantes de Controls_Manager (includes/managers/controls.php), cada uma com o seu próprio PHPDoc de uma linha no código-fonte (citado tal-qual abaixo) e devolvida por Controls_Manager::get_controls_names() (método real, usado internamente por register_controls() da própria classe para instanciar Control_{Nome} por convenção de nome de classe — str_replace('_',' ',ucwords(...)), confirmado). A ability list-control-types proposta devolveria exactamente este catálogo.

3.1 Controlos de dados simples/compostos (get_controls_names())

Constante Valor Descrição (PHPDoc real) Parâmetros confirmados em uso real (heading.php)
TEXT text "Text control." Não exemplificado directamente em heading.php (usa TEXTAREA); campo de linha única, aceita label/default/placeholder/dynamic por convenção Elementor
NUMBER number "Number control." Não exemplificado
TEXTAREA textarea "Textarea control." heading.php: label, type, ai:{type:'text'} (marcador de compatibilidade com AI writer nativo do Elementor), dynamic:{active:true}, placeholder, default
SELECT select "Select control." heading.php: label, options (mapa valor => label), default, condition (ex. 'size!' => 'default', esconde o controlo condicionalmente a outro valor)
SWITCHER switcher "Switcher control." Não exemplificado; toggle on/off, aceita label_on/label_off/return_value por convenção
BUTTON button "Button control." Não exemplificado
HIDDEN hidden "Hidden control." Usado internamente por Widget_Base::register_skin_control() ('_skin' quando só há 1 skin)
HEADING heading "Heading control." Separador de UI, sem valor associado
RAW_HTML raw_html "Raw HTML control." Usado extensivamente no próprio Controls_Manager para avisos de upsell Pro; aceita raw (string HTML)
NOTICE/DEPRECATED_NOTICE/ALERT notice/deprecated_notice/alert Controlos de aviso/UI informativa Widget_Base::add_deprecation_message() usa ALERT com alert_type/content
POPOVER_TOGGLE/SECTION/TAB/TABS/DIVIDER idem Controlos estruturais de UI DIVIDER usado em heading.php: ['type' => Controls_Manager::DIVIDER], sem outros parâmetros
COLOR color "Color control." heading.php: label, global:{default: Global_Colors::COLOR_PRIMARY} (liga a paleta global do kit), selectors
MEDIA media "Media control." Não exemplificado em heading.php; aceita objecto {url, id}, tipicamente com media_types:['image']
SLIDER slider "Slider control." heading.php (title_hover_color_transition_duration): size_units:['s','ms','custom'], default:{unit:'s'}, selectors com {{SIZE}}{{UNIT}}
DIMENSIONS/IMAGE_DIMENSIONS dimensions/image_dimensions Controlos de 4 valores (top/right/bottom/left) + unidade Não exemplificado
CHOOSE choose "Choose control." heading.php (align): options (mapa valor => {title, icon}), selectors_dictionary (remapeamento de valor consoante RTL/LTR), toggle implícito
VISUAL_CHOICE visual_choice "Visual_Choice control." Não exemplificado
WYSIWYG wysiwyg "WYSIWYG control." Editor rich-text (TinyMCE); não exemplificado
CODE code "Code control." Editor CodeMirror com language param; não exemplificado
FONT font "Font control." Não exemplificado
GAPS gaps "Gaps control." Não exemplificado (par row/column)
WP_WIDGET wp_widget "WordPress widget control." Embrulha um widget clássico WordPress dentro do Elementor
URL url "URL control." heading.php: label, dynamic:{active:true}, default:{url:''}
REPEATER repeater "Repeater control." Não exemplificado em heading.php; array de sub-campos repetíveis (padrão para listas — ex. itens de accordion, slides)
ICON/ICONS icon/icons Selecção de ícone (biblioteca antiga vs nova unificada, ICONS é o formato actual) Não exemplificado
GALLERY gallery "Gallery control." Array de attachments; não exemplificado
STRUCTURE structure "Structure control." Selector visual de estrutura de colunas
SELECT2 select2 "Select2 control." Multi-select com pesquisa
DATE_TIME date_time "Date/Time control." Não exemplificado
BOX_SHADOW/TEXT_SHADOW box_shadow/text_shadow Controlos de sombra (também disponíveis como group control, ver 3.2) heading.php usa a variante group (Group_Control_Text_Shadow), não o controlo simples directamente
ANIMATION/HOVER_ANIMATION/EXIT_ANIMATION idem Dropdown de animações de entrada/hover/saída Não exemplificado

3.2 Group controls (get_groups_names()) — controlos compostos multi-campo

Controls_Manager::get_groups_names() (confirmado, includes/managers/controls.php): background, border, typography, image-size, box-shadow, css-filter, text-shadow, flex-container, grid-container, flex-item, text-stroke. Instanciados via Group_Control_{Nome} (mesma convenção de nome de classe) e adicionados com add_group_control(Group_Control_X::get_type(), [...]) — confirmado em uso real por heading.php para typography ('name' => 'typography', 'global' => ['default' => Global_Typography::TYPOGRAPHY_PRIMARY], 'selector' => '{{WRAPPER}} .elementor-heading-title'), text_stroke e text_shadow (ambos só name+selector).

Nota honesta sobre o nível de detalhe desta tabela: os parâmetros confirmados vêm de dois sítios — (a) leitura directa do PHPDoc de cada constante em Controls_Manager (garante que o tipo existe e o nome é exacto), e (b) o subconjunto de tipos efectivamente usados em Widget_Heading::register_controls() (garante o shape exacto dos parâmetros aceites nesses casos). Para os tipos não exemplificados em heading.php (ex. MEDIA, REPEATER, WYSIWYG, GALLERY), o shape descrito é conhecimento geral do ecossistema Elementor, não confirmado linha-a-linha nesta sessão por leitura do respectivo ficheiro Control_{Nome}.php — uma implementação real do list-control-types deveria ler cada uma dessas ~35 classes individualmente (includes/controls/*.php) antes de publicar o schema completo, algo fora do âmbito desta tarefa.


4. validate-widget-spec — o que validar [DESENHO PRÓPRIO, sobre APIs [REAL]]

Lista de validações propostas, cada uma ancorada num método real do Elementor:

# Validação API real usada Comportamento em caso de falha
1 name é um slug válido ([a-z][a-z0-9_]*) [DESENHO PRÓPRIO] — regra nossa, sem equivalente Elementor explícito (Elementor não valida o formato do nome, só usa-o como chave de array) Rejeita antes de qualquer outra verificação
2 name não colide com um widget já registado (nem custom nem core) Plugin::$instance->widgets_manager->get_widget_types( $name ) — confirmado, includes/managers/widgets.php:356: com argumento devolve a instância registada ou null; sem argumento devolve o array completo. Um null significa "livre" Rejeita com widget_name_taken se não for null — crítico: sem este check, add_control_to_stack() internamente ainda protege controlos duplicados (ver #4), mas dois widgets com o mesmo name fariam o segundo silenciosamente substituir o primeiro no array _widget_types[] (última chamada a register() ganha, sem aviso)
3 Cada controls[].type está em Controls_Manager::get_controls_names() (secção 3.1) Controls_Manager::get_control($type) — confirmado, devolve false se o tipo não existir (includes/managers/controls.php, get_control()) Rejeita com unknown_control_type, lista os tipos válidos no erro
4 controls[].id únicos dentro do mesmo widget Confirmado directamente no código real: Controls_Manager::add_control_to_stack() já faz este check em runtime — if ( ! $options['overwrite'] && isset(...) ) { _doing_it_wrong(...'Cannot redeclare control with same name "%s"'...); return false; } (includes/managers/controls.php). Um gerador que ignorasse isto produziria um widget que silenciosamente perde controlos duplicados (o segundo add_control() falha e devolve false, sem excepção) O nosso validate-widget-spec replica este check antes da compilação, para dar ao agente um erro estruturado em vez de deixar o _doing_it_wrong() (que só aparece em WP_DEBUG) engolir o problema silenciosamente em produção
5 category está em Elements_Manager::get_categories() (secção 2) ou é uma categoria custom já registada via elementor/elements/categories_registered Elements_Manager::get_categories() — confirmado, includes/managers/elements.php:121 Aviso (não bloqueia) — uma categoria desconhecida faz o Elementor simplesmente não mostrar o widget em nenhum grupo do painel (falha silenciosa da UI, não fatal); melhor avisar do que bloquear, dado que uma categoria custom pode legitimamente só existir depois de o widget ser gerado (ordem de hooks)
6 Todo {{control_id}} referenciado em render_template corresponde a um controls[].id declarado [DESENHO PRÓPRIO] — nenhum equivalente Elementor (o render() de um widget core é PHP livre, escrito à mão, nunca templated) Rejeita com undefined_placeholder; evita gerar um render() que faz $settings['campo_inexistente'] (PHP notice em runtime, não fatal, mas widget quebrado silenciosamente)
7 css/js (se fornecidos) não contêm tags de abertura PHP Reaproveita directamente a defesa já confirmada em EMCP_Tools_Widget_Store real (docs/06 §3.9 / §4 "Copiar quase 1:1" item 6): `preg_replace('/<?(?:php =)?/i', '', $out)`
8 Se render_template for omitido e um render_php raw for fornecido em alternativa Reaproveita o validador de 3 camadas já confirmado em EMCP_Tools_PHP_Snippet_Validator (docs/06 §3.3 — parse + scan de segurança critical/warning) Rejeita com invalid_php/unsafe_php + relatório de findings, mesmo contrato de resposta já usado por validate-php-snippet

Decisão de design explícita: preferir sempre render_template (placeholders, sem PHP) sobre render_php (PHP raw validado pelo scanner). O primeiro elimina uma classe inteira de vulnerabilidades (não há PHP para injectar); o segundo só deveria existir como escape-hatch para casos que o template simples não cobre (loops, condicionais complexos) — e, quando usado, herda exactamente o mesmo modelo de aprovação humana descrito na secção 6.


5. O compilador spec→PHP — desenho próprio [DESENHO PRÓPRIO]

Esta é a peça central que o EMCP Pro tem (EMCP_Tools_Widget_Generator) e nós não podemos ler — tudo nesta secção é proposta própria, fundamentada apenas nos contratos reais das secções 2-3.

5.1 Esqueleto da classe gerada

<?php
// wp-content/{sandbox}/widgets/{id}/widget.php — gerado, nunca editado à mão
class EMCP_Widget_{id} extends \Elementor\Widget_Base {

    public function get_name() { return 'emcp_custom_{id}'; }
    public function get_title() { return '{spec.title, esc}'; }
    public function get_icon() { return '{spec.icon, esc}'; }
    public function get_categories() { return [ '{spec.category, esc}' ]; }
    public function get_keywords() { return [ /* spec.keywords, cada um esc_attr */ ]; }

    protected function register_controls() {
        // por cada item de spec.sections[], nesta ordem:
        $this->start_controls_section( '{section.id}', [
            'label' => '{section.label, esc}',
            'tab'   => '{section.tab}',  // Controls_Manager::TAB_CONTENT|TAB_STYLE|...
        ] );
        // por cada spec.controls[] com controls[].section === section.id:
        $this->add_control( '{control.id}', [
            'label'     => '{control.label, esc}',
            'type'      => \Elementor\Controls_Manager::{TYPE_CONST},
            'default'   => {control.default, json-safe},
            'options'   => {control.options},      // só se aplicável ao tipo
            'selectors' => {control.selectors},     // só se preenchido na spec
        ] );
        $this->end_controls_section();
    }

    protected function render() {
        $settings = $this->get_settings_for_display();
        $__out = '{spec.render_template com placeholders ainda por trocar}';
        // por cada {{control_id}}: substituir por uma função de escape
        // apropriada ao TIPO do controlo (ver tabela 5.2), NUNCA um output cru
        echo $__out; // phpcs:ignore — já escapado por control-type na compilação
    }
}

Os nomes de classe (EMCP_Widget_{id}) e de widget (emcp_custom_{id}) não são invenção desta secção — são reaproveitados tal-qual do padrão já confirmado em EMCP_Tools_Widget_Store::create() (docs/06 §3.9, citação directa: "insere o post primeiro (para o ID poder semear nomes únicos de classe EMCP_Widget_{id}/widget emcp_custom_{id})") — o id vem do post ID do CPT emcp_widget real (secção 6), garantindo unicidade sem precisar de gerar UUIDs.

5.2 Mapeamento tipo de controlo → função de escape no render() [DESENHO PRÓPRIO]

A parte mais sensível do compilador — cada placeholder {{control_id}} no render_template é substituído em tempo de compilação por PHP que já aplica a escape correcta, nunca por output cru:

Tipo de controlo Escape aplicado no PHP gerado
text, textarea, select, hidden esc_html( $settings['{id}'] )
url esc_url( $settings['{id}']['url'] ) (o valor é um array {url,...}, confirmado pelo shape usado em heading.php link)
color esc_attr( $settings['{id}'] ) (usado tipicamente dentro de style="...")
media esc_url( $settings['{id}']['url'] ) para o URL; absint( $settings['{id}']['id'] ) se usado para wp_get_attachment_image()
wysiwyg wp_kses_post( $settings['{id}'] ) — permite HTML controlado, mesma função usada por Widget_Heading::render() real para o campo title
number, slider floatval/intval( $settings['{id}'] ) (nunca esc_html, para não citar acidentalmente)
repeater, gallery Não elegível para substituição directa de placeholder — requer um foreach no render_template; fora de âmbito de um compilador v1 (ver secção 8), recomenda-se cair para render_php validado nesse caso

Este mapeamento é o equivalente, do lado do gerador de widgets, à "defesa de injecção de tag PHP" que EMCP_Tools_Widget_Store já aplica aos ficheiros .css/.js gerados (secção 4, item 7) — a mesma filosofia ("o compilador nunca confia no conteúdo, escapa sempre pelo tipo declarado") aplicada a uma superfície diferente (HTML de saída em vez de ficheiros estáticos).

5.3 Registo do widget compilado — reaproveita o hook real (secção 2)

O ficheiro gerado é só definido (via include_once, manifest-only — secção 6); a instanciação e registo acontece no hook real elementor/widgets/register:

add_action( 'elementor/widgets/register', function( $widgets_manager ) {
    foreach ( /* manifest de widgets activos */ as $entry ) {
        $class = $entry['class_name'];
        if ( class_exists( $class ) ) {
            $widgets_manager->register( new $class() );
        }
    }
} );

Exactamente o padrão já confirmado em docs/04-THEMER.md §10 para os widgets dinâmicos do Themer (class-themer-widgets.php: require_once tardio + registo dentro do mesmo hook).


6. Sandbox e ciclo de vida — reaproveitar o modelo já confirmado em código real [REAL]

Esta secção é a que menos inventa. EMCP_Tools_Widget_Store e EMCP_Tools_Widget_Loader existem na build Free (confirmado por leitura integral em docs/06-SANDBOX-CUSTOM-CODE.md §3.9-3.10) — só a peça que os alimenta (o compilador, secção 5) e a peça que os expõe a agentes de IA (secção 7) é que faltam. Reaproveitamos aqui, explicitamente citado de docs/06, o modelo de 4 camadas de segurança já validado nesta série para PHP gerado por IA (o mesmo aplicado ao Themer PHP, docs/04-THEMER.md §11, e aos PHP Snippets, docs/06 §1/§3.2-3.3):

6.1 Camada 1 — Manifest-only lookup [REAL]

EMCP_Tools_Widget_Store::rebuild_manifest()/read_manifest() (confirmado, docs/06 §3.9) mantêm wp-content/emcp-sandbox/widgets-manifest.json — array de entradas só dos widgets publish (activos). EMCP_Tools_Widget_Loader::register_widgets() nunca faz scan de directório — lê só o manifest. Idêntico ao padrão do PHP_Snippet_Loader (docs/06 §3.2).

6.2 Camada 2 — Tamper guard sha256 [REAL]

Cada entrada do manifest guarda _emcp_php_hash/_emcp_css_hash/_emcp_js_hash (confirmado, meta keys reais de EMCP_Tools_Widget_Store). Antes de include_once, o loader recalcula hash('sha256', file_get_contents($path)) e compara — um ficheiro alterado por fora do fluxo normal (ex. edição manual FTP, ou um bug de escrita concorrente) é ignorado silenciosamente em vez de executado.

6.3 Camada 3 — Path containment guard [REAL]

O mesmo strpos(normalize(path), normalize(sandbox_dir)) === 0 já confirmado no snippet loader (docs/06 §3.2) aplica-se aqui — uma entrada de manifest cujo php_path aponte para fora de wp-content/emcp-sandbox/widgets/ é recusada antes de qualquer include_once.

6.4 Camada 4 — runtime_validate() + shutdown fatal-recovery [REAL]

Peça específica de widgets, sem equivalente directo nos PHP Snippets — confirmado em docs/06 §3.9: EMCP_Tools_Widget_Store::runtime_validate() instancia a classe gerada e chama $instance->get_stack() — o mesmo método get_stack()/init_controls() confirmado nesta sessão directamente em widget-base.php (secção 2) — antes de deixar o widget ir para produção. Se o register_controls() gerado rebentar (erro de sintaxe residual não apanhado pelo token_get_all() do validador de PHP, ou uma chamada a add_control() com tipo inválido que escapou à validação da spec), é apanhado aqui, dentro de um contexto controlado, em vez de dar white-screen no painel do editor Elementor para todos os utilizadores desse site. safeguard_active() (confirmado, docs/06 §3.9) degrada automaticamente para draft se esta validação falhar — nunca deixa algo partido ficar "activo". Adicionalmente, mark_error() + o shutdown handler do EMCP_Tools_Widget_Loader (register_shutdown_function, mesma rede de segurança confirmada para snippets em docs/06 §3.2) apanham fatais que só se manifestam já em runtime de front-end (não apanhados por runtime_validate() em tempo de activação).

6.5 Decisão de segurança: aprovação humana obrigatória antes de activar [DESENHO PRÓPRIO, precedente REAL]

Recomendação: sim, replicar a filosofia "zero tool de auto-activação" já aplicada ao Themer PHP e aos PHP Snippets no Free. Justificação, ambas citações directas de código real já confirmado nesta série:

  • Themer PHP (docs/04-THEMER.md §2, comentário de topo do ficheiro citado tal-qual): "AI authors + validates DRAFT PHP templates; there is intentionally no attach tool — a human selects a template in the Themer metabox (the execution gate)".
  • PHP Snippets (docs/06 §1, citação directa): "There is intentionally no 'activate' tool: a snippet created via MCP is an inactive draft until a human administrator reviews it and activates it in the Sandbox admin screen".

Ambos os subsistemas reais deste plugin, que geram/executam código a partir de input de IA, escolheram consistentemente nunca expor a transição draft→activo como uma ability MCP — só a UI de admin (com nonce + sessão humana) o consegue fazer. EMCP_Tools_Widget_Store::set_status() (confirmado, docs/06 §3.9, mesmo padrão de PHP_Snippet_Store::set_status(), §3.1) já é esse portão — é chamado hoje só pelo handler AJAX do admin, nunca por uma ability.

Tensão a resolver explicitamente: o nome set-widget-status está no catálogo de 8 tools da captura de ecrã do EMCP Pro (secção 0) — o que sugere que o EMCP Pro pode de facto expor a activação via MCP, divergindo da filosofia "zero tool de attach" aplicada ao resto do Free. Como não temos o código Pro para confirmar o comportamento real dessa tool (permission_callback exacto, se força runtime_validate(), se pede confirmação extra), este blueprint recomenda explicitamente NÃO seguir esse precedente sem mais garantias, e propõe duas alternativas concretas na secção 7 (a decisão B é a recomendada).


7. Tabela de abilities propostas [DESENHO PRÓPRIO sobre camadas [REAL]``]`

Os 8 nomes seguem exactamente o catálogo visível na UI admin do EMCP Pro (secção 0) — usados só como naming-target. Schema/comportamento/permissões são propostas próprias, construídas em cima de EMCP_Tools_Widget_Store ([REAL], secção 6) e Controls_Manager/Elements_Manager/Widgets_Manager ([REAL], secções 2-4).

Ability Input (proposto) O que faz Permissão proposta readonly / destructive
list-control-types {} Devolve o catálogo da secção 3 (tipos simples + group controls), com o schema de parâmetros por tipo (marcando explicitamente na resposta quais foram confirmados por uso real vs. inferidos, secção 3.2) edit_posts (é só metadata de descoberta, paralelo ao list-tools do dispatcher, docs/00 §4) readonly, idempotent
validate-widget-spec { spec: object } (schema da secção 1) Corre as 8 validações da secção 4, devolve {valid, findings[]} — pensado para iterar antes de create-custom-widget, mesmo contrato de UX que validate-php-snippet já usa (docs/06 §1) manage_options (leitura+análise, sem escrita) readonly, idempotent
create-custom-widget { spec: object } Valida primeiro (rejeita com o mesmo relatório de validate-widget-spec em caso de falha — reaproveita normalize_write_result(), padrão confirmado em docs/06 §1); compila via o gerador (secção 5); grava via EMCP_Tools_Widget_Store::create($spec, false) — $active forçado a false, sempre, independentemente de qualquer flag no input manage_options E unfiltered_html (mesma dupla capability já usada por create-php-snippet, docs/06 §1 — justificação idêntica: gerar código PHP que corre no servidor) write, não destructive, não idempotent
update-custom-widget { widget_id: int, spec: object (parcial) } Re-valida a spec mesclada; recompila; se o widget já estava active, corre runtime_validate() (secção 6.4) e degrada para draft automaticamente em caso de falha (mesmo padrão de PHP_Snippet_Store::update(), docs/06 §3.1) manage_options + unfiltered_html write, não idempotent
get-custom-widget { widget_id: int } Devolve a spec completa (fonte-de-verdade _emcp_spec), estado (draft/active), hash do PHP compilado, e o último erro registado (_emcp_last_error) — todos campos reais já confirmados em EMCP_Tools_Widget_Store manage_options readonly, idempotent
list-custom-widgets { status?: 'active'|'draft'|'any' } Lista widgets custom com nome/título/categoria/estado manage_options readonly, idempotent
set-widget-status Não recomendada como MCP ability (ver decisão B abaixo) — se implementada apesar disso: { widget_id: int, status: 'draft' } apenas (nunca 'active') Só permite desactivar via MCP; activar continua exclusivo da UI admin manage_options + unfiltered_html write, não destructive (desactivar nunca perde dados — o .php fica em disco, só sai do manifest)
delete-custom-widget { widget_id: int, force?: bool } Apaga o post CPT + os ficheiros do widget (widget.php/style.css/script.js) + reconstrói o manifest — mesmo padrão de PHP_Snippet_Store::delete() (docs/06 §3.1) manage_options + unfiltered_html destructive, idempotent

Decisão explícita sobre set-widget-status (retoma a tensão da secção 6.5):

  • Opção A (seguir o naming do catálogo Pro): set-widget-status aceita 'active' e 'draft' via MCP, com runtime_validate() sempre corrido antes de qualquer activação e falha bloqueante. Mais próximo do que o EMCP Pro parece expor (só pelo nome).
  • Opção B — recomendada: set-widget-status só aceita 'draft' via MCP (desactivação, sempre segura e reversível). Activar um widget ('active') nunca é alcançável por uma ability — só pelo mesmo botão de admin que já existe para EMCP_Tools_Widget_Store hoje. Justificação: consistência com os dois outros subsistemas reais de código-gerado-por-IA deste plugin (Themer PHP e PHP Snippets), ambos deliberadamente sem essa capacidade via MCP; e o facto de a spec ser JSON estruturado (não PHP livre) não elimina o risco — um render_template pode ainda produzir HTML/CSS malicioso se a validação da secção 4 tiver um buraco, e um agente com acesso MCP total (manage_options+unfiltered_html) que consiga também activar sem revisão humana teria, de facto, execução arbitrária de HTML/CSS no site — a mesma classe de risco que motivou a decisão "zero tool de attach" nos outros dois subsistemas.

8. Blueprint para réplica

Copiar quase 1:1 (já é código real, [REAL])

  1. EMCP_Tools_Widget_Store + EMCP_Tools_Widget_Loader inteiros — manifest-only, tamper guard sha256, path containment, runtime_validate(), safeguard_active(), mark_error(), shutdown handler. É a peça mais valiosa de todo este blueprint precisamente porque já não é desenho — é código já testado em produção no Free (confirmado nesta série).
  2. O hook real de registo (elementor/widgets/register → Widgets_Manager::register()) — não há nada a inventar aqui, é a única forma suportada do Elementor.
  3. A defesa de injecção de tag PHP em assets .css/.js (preg_replace('/<\?(?:php|=)?/i', '', $out)) — já confirmada e reaproveitável tal-qual.
  4. O validador de PHP de 3 camadas (EMCP_Tools_PHP_Snippet_Validator) — reaproveitar directamente para o caminho de escape-hatch render_php (secção 4, item 8).

Construir de raiz ([DESENHO PRÓPRIO], sequência recomendada por dependência)

  1. list-control-types primeiro — zero risco, zero dependência de escrita, e é o que validate-widget-spec e o compilador (passo 3) vão consumir para saber o que é válido. Nesta fase, ler efectivamente os ~35 ficheiros includes/controls/Control_*.php individuais para fechar a lacuna identificada na secção 3.2 (parâmetros não confirmados por leitura directa).
  2. validate-widget-spec — as 8 verificações da secção 4, todas independentes entre si, implementáveis e testáveis isoladamente antes de qualquer geração de PHP.
  3. O compilador — começar só com os controlos de conteúdo mais simples (text, textarea, select, switcher, url, color, number) e o mapeamento de escape da secção 5.2 para esses tipos; adicionar media a seguir (precisa de wp_get_attachment_image()/ esc_url()); deixar os group controls (typography/background/border, secção 3.2) para uma segunda iteração — têm parâmetros (name+selector+fields_options) mais complexos que os controlos simples e não são necessários para um MVP funcional.
  4. create/get/list/update-custom-widget sobre o Widget_Store já copiado (passo 0) — sempre $active = false forçado em create, nunca negociável via input.
  5. delete-custom-widget — trivial depois do passo 4.
  6. set-widget-status — implementar a Opção B da secção 7 (só desactivação via MCP); a activação continua um botão de admin humano ligado directamente a set_status('active') do Widget_Store já existente.

Deixar de fora ou adiar explicitamente

  • content_template() (preview Backbone.js no editor sem round-trip ao servidor) — um widget gerado sem isto ainda funciona perfeitamente (o Elementor faz fallback a um pedido AJAX de render() PHP no editor), só perde fluidez de preview em tempo real. Não é bloqueante para v1.
  • repeater/gallery no compilador de render_template (secção 5.2) — exigem lógica de loop que um simples find-and-replace de placeholders não cobre; ficam atrás do escape-hatch render_php validado até haver um motor de template mais rico (ex. Mustache-like {{#each}}).
  • Skins (Widget_Base::register_skins()/add_skin()) — mecanismo real e confirmado, mas de complexidade desproporcional face ao valor para um gerador orientado a spec simples; um widget gerado não precisa de múltiplos skins visuais geridos por IA.
  • get_upsale_data()/promoções Pro — específico do próprio Elementor Core anunciar o Elementor Pro; irrelevante para widgets gerados por um sistema terceiro.

9. Fonte

Elementor 4.2.2 (Free), lido directamente nesta sessão via ssh://server/home/ealmeida/emanuelalmeida.pt/wp-content/plugins/elementor/:

  • includes/base/widget-base.php (integral — Widget_Base, incl. get_stack(), init_controls(), register_skin_control(), render_content(), get_initial_config(), add_deprecation_message())
  • includes/managers/controls.php (integral — Controls_Manager, todas as constantes de tipo simples e de grupo, get_controls_names(), get_groups_names(), register_controls(), add_control_to_stack() incl. o check de duplicado _doing_it_wrong, register(), get_control())
  • includes/managers/widgets.php (excertos: register(Widget_Base $widget_instance) linha 261, get_widget_types($widget_name = null) linha 356 lido na íntegra, hook elementor/widgets/register linha 159)
  • includes/managers/elements.php (excertos: get_categories()/add_category(), init_categories() lido na íntegra com a lista completa de categorias nativas desta versão)
  • includes/widgets/heading.php (integral — Widget_Heading, exemplo real e completo de register_controls()/render()/content_template(), usado como fonte dos parâmetros confirmados por uso real na tabela da secção 3.1)

Cruzado com esta mesma série de documentos (código real do emcp-tools, já lido em sessões anteriores, não relido nesta tarefa):

  • docs/06-SANDBOX-CUSTOM-CODE.md §1, §3.1-3.3, §3.9-3.10, §4 — EMCP_Tools_PHP_Snippet_Abilities, EMCP_Tools_PHP_Snippet_Store, EMCP_Tools_PHP_Snippet_Loader, EMCP_Tools_PHP_Snippet_Validator, e criticamente EMCP_Tools_Widget_Store/EMCP_Tools_Widget_Loader (confirmados presentes na build Free, ao contrário de EMCP_Tools_Widget_Generator/EMCP_Tools_Widget_Builder_Abilities, confirmados ausentes)
  • docs/04-THEMER.md §2, §11 — EMCP_Tools_Themer_PHP_Abilities, EMCP_Tools_Themer_PHP_Store, e o hook real elementor/widgets/register já usado por class-themer-widgets.php/ class-themer-widget-classes.php (widgets Elementor dinâmicos do próprio Themer, base real adicional confirmando o contrato da secção 2)
  • docs/00-ARQUITECTURA.md §5 — normalize_result()/normalize_write_result(), padrão de envelope de resposta reaproveitado nas tabelas de ability da secção 7
  • docs/11-WOOCOMMERCE-BLUEPRINT.md — usado como referência de estilo e formato deste tipo de documento "blueprint, não auditoria" (secção 0 explícita, tabelas de ability propostas com coluna de risco/destructive, secção final "Blueprint para réplica" com copiar/construir/omitir)

Não lido nesta tarefa (lacunas explícitas, ver secção 3.2 e 8): os ~35 ficheiros individuais includes/controls/Control_*.php e os 11 ficheiros includes/controls/groups/Group_Control_*.php do Elementor (schema exacto por tipo de controlo, além dos exemplificados em heading.php); qualquer código do EMCP Pro (EMCP_Tools_Widget_Generator, EMCP_Tools_Widget_Builder_Abilities) — fisicamente ausente da árvore Free instalada, confirmado por tentativa de leitura em sessão anterior desta série (docs/06 §3.9), não retentado nesta tarefa.