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.
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:
- 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), eincludes/widgets/heading.php(Widget_Heading, um widget core real como exemplo concreto deregister_controls()/render()). - Padrões de segurança já confirmados (código real, Free) em
docs/06-SANDBOX-CUSTOM-CODE.md— nomeadamenteEMCP_Tools_Widget_Store(§3.9, existe na build Free, 839 linhas lidas na íntegra) eEMCP_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). - 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-statusaceita'active'e'draft'via MCP, comruntime_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-statussó 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 paraEMCP_Tools_Widget_Storehoje. 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 — umrender_templatepode 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])
EMCP_Tools_Widget_Store+EMCP_Tools_Widget_Loaderinteiros — 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).- O hook real de registo (
elementor/widgets/register→Widgets_Manager::register()) — não há nada a inventar aqui, é a única forma suportada do Elementor. - A defesa de injecção de tag PHP em assets
.css/.js(preg_replace('/<\?(?:php|=)?/i', '', $out)) — já confirmada e reaproveitável tal-qual. - O validador de PHP de 3 camadas (
EMCP_Tools_PHP_Snippet_Validator) — reaproveitar directamente para o caminho de escape-hatchrender_php(secção 4, item 8).
Construir de raiz ([DESENHO PRÓPRIO], sequência recomendada por dependência)
list-control-typesprimeiro — zero risco, zero dependência de escrita, e é o quevalidate-widget-spece o compilador (passo 3) vão consumir para saber o que é válido. Nesta fase, ler efectivamente os ~35 ficheirosincludes/controls/Control_*.phpindividuais para fechar a lacuna identificada na secção 3.2 (parâmetros não confirmados por leitura directa).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.- 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; adicionarmediaa seguir (precisa dewp_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. create/get/list/update-custom-widgetsobre oWidget_Storejá copiado (passo 0) — sempre$active = falseforçado emcreate, nunca negociável via input.delete-custom-widget— trivial depois do passo 4.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 aset_status('active')doWidget_Storejá 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 derender()PHP no editor), só perde fluidez de preview em tempo real. Não é bloqueante para v1.repeater/galleryno compilador derender_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-hatchrender_phpvalidado 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, hookelementor/widgets/registerlinha 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 deregister_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 criticamenteEMCP_Tools_Widget_Store/EMCP_Tools_Widget_Loader(confirmados presentes na build Free, ao contrário deEMCP_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 realelementor/widgets/registerjá usado porclass-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 7docs/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.