# 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):
```json
{
"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": "
{{quote_text}}
{{author_name}} ",
"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)` | Nunca bloqueia — **sanitiza** silenciosamente (remove a tag), documentado explicitamente na resposta da ability para o agente saber que algo foi alterado |
| 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
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`:
```php
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.