Files
emcp-tools-mapping/docs/01-ELEMENTOR-CLASSICO.md
T
Claude Code 8ada367bd0 docs: mapeamento completo do EMCP Tools (11 docs, ~6070 linhas)
Especificação funcional do plugin emcp-tools v3.12.1 (msrbuilds/elementor-mcp,
GPL-2.0-or-later) via leitura directa do código-fonte em emanuelalmeida.pt.

- 00: arquitectura (bootstrap, ability registrar, dispatcher, MCP adapter)
- 01: Elementor classico (paginas, layout, widgets, templates, globals)
- 02: Elementor Atomic v4 + Gutenberg
- 03: WordPress core (conteudo, media, settings, temas)
- 04: Themer (CPT, condicoes, render, PHP templates)
- 05: Redirects + change ledger unificado (rollback)
- 06: Sandbox PHP snippets + custom widgets
- 07: Filesystem/DB/WP-CLI/Security/Performance (maior risco)
- 08: Integracoes terceiros (ACF, Meta Box, forms, SEO)
- 09: Stock images + Cloud + OAuth
- 10: Sistema de modulos + inventario Pro-only (30 classes)
- INDEX: sintese, sequencia de construcao, tabela de risco

Produzido por 10 subagentes code-explorer em paralelo + revisao cruzada de
consistencia. Cada doc inclui blueprint de replica (copiar/simplificar/omitir).
2026-08-19 06:41:04 +01:00

633 lines
56 KiB
Markdown

# 01 — Elementor Clássico (não-atómico): páginas, layout, widgets, templates, globals, custom code, composite
Fonte: leitura directa do código-fonte `emcp-tools` v3.12.1 (build Free), instalado em
`emanuelalmeida.pt` (`/home/ealmeida/emanuelalmeida.pt/wp-content/plugins/emcp-tools/`),
19-08-2026. Cobre as classes de abilities que operam sobre o modelo de dados Elementor
**clássico** (`_elementor_data`, árvore `container`/`widget`/`section`/`column`), incluindo os
grupos de leitura/escrita das **Global Classes** do Elementor 4.0+ (que já não são "clássicas"
mas vivem no mesmo `_elementor_data`/kit e não fazem parte do sistema de props atómicas
tratado no doc 02). Todas as classes aqui descritas registam-se dentro do bloco
`if ( $elementor_active )` de `class-ability-registrar.php` (linha ~452), sem nenhum outro gate
de módulo — só o Elementor (free) precisa de estar activo.
## ⚠️ Nota importante para a batch — onde vive o CRUD de widgets custom
`class-widget-abilities.php` (`EMCP_Tools_Widget_Abilities`) **NÃO contém** nenhuma tool de
criação/edição/eliminação de widgets custom (`create-custom-widget`, `update-custom-widget`,
`get-custom-widget`, `list-custom-widgets`, `set-widget-status`, `delete-custom-widget` — a
lista de 16 tools "Widget/Block Builder" identificada em `skill://emcp-tools` §2.14). Esta
classe cobre **só colocação/actualização de instâncias de widget numa página** — três tools:
`add-free-widget`, `add-pro-widget` (ambas catalog-backed, inserem um widget *já existente* no
registo do Elementor) e `update-widget` (edita definições de uma instância já colocada). O CRUD
de definição de widgets custom (a "fábrica" que cria um NOVO tipo de widget PHP/JS a partir de
um spec) vive noutro módulo — **confirmado por grep ao `class-ability-registrar.php`, não faz
parte de nenhuma das 10 classes desta tarefa** — quase certamente no sandbox de widgets/blocos
custom tratado no doc 06 (`EMCP_Tools_Sandbox_*`). Doc06 deve confirmar isto ao ler o registrar
completo; aqui fica o achado negativo registado para não haver dupla cobertura nem lacuna.
---
## 1. `EMCP_Tools_Page_Abilities` — `includes/abilities/class-page-abilities.php`
**Condição de registo:** dentro de `if ( $elementor_active )`, sempre (sem gate adicional).
Construtor recebe `EMCP_Tools_Data $data` e `EMCP_Tools_Element_Factory $factory` (injectados
pelo registrar). 5 tools, todas prefixadas `emcp-tools/`.
| Tool | Input (resumo) | O que faz | `permission_callback` | readonly / destructive |
|---|---|---|---|---|
| `create-page` | `title*` (string), `status` (`draft`\|`publish`, default draft), `post_type` (`page`\|`post`, default page), `template` (slug), `content` (array de elementos, opcional) | `wp_insert_post()` com `_elementor_edit_mode=builder` + `_elementor_template_type=wp-{post_type}`; grava `content` (ou `[]` se omitido) via `EMCP_Tools_Data::save_page_data()`; devolve `post_id`, `edit_url`, `preview_url`. | `check_create_permission`: `publish_pages` \|\| `edit_pages` | não-readonly, não-destructive, não-idempotente |
| `update-page-settings` | `post_id*`, `settings*` (objecto livre) | Delegado 1:1 a `EMCP_Tools_Data::save_page_settings()` — grava definições ao nível de página (background, padding, custom CSS, layout) via `Document::save(['settings'=>...])` com fallback a merge em `_elementor_page_settings`. | `check_edit_permission`: `edit_posts` + (se `post_id`) `edit_post` desse post | não-readonly, não-destructive, idempotente |
| `delete-page-content` | `post_id*` | `save_page_data($post_id, [])` — **limpa TODO o conteúdo Elementor da página**, mantendo a página em si (post continua a existir). | `check_delete_permission`: `edit_posts` **E** `delete_posts`, mais (se `post_id`) `edit_post` **E** `delete_post` desse post — a única classe do doc que exige capability de eliminação para uma operação que tecnicamente só edita meta, porque o efeito é irreversível sem o change-ledger | não-readonly, **destructive**, idempotente |
| `import-template` | `post_id*`, `template_json*` (array de elementos Elementor), `position` (default -1 = append) | Lê a página actual, `reassign_ids()` a todo o `template_json` (evita colisão de IDs), insere no array (append ou `array_splice` na posição) e grava. Devolve `elements_count` (contagem recursiva via `count_elements()`). | `check_edit_permission` | não-readonly, não-destructive, não-idempotente |
| `export-page` | `post_id*` | Devolve `get_page_data($post_id)` verbatim como `json` — export completo da árvore Elementor da página, reimportável via `import-template`/`apply-template`. | `check_edit_permission` | **readonly**, idempotente |
**Achado de design:** `create-page` grava sempre `_elementor_data` mesmo quando `content` é
omitido (`save_page_data($post_id, [])`) — isto **inicializa** a meta em vez de a deixar por
criar, o que é relevante porque `EMCP_Tools_Data::get_page_data()` trata "meta ausente" e "meta
`[]`" da mesma forma (array vazio), mas só a segunda garante que o Elementor reconhece a página
como "Editada com Elementor" desde o primeiro save.
---
## 2. `EMCP_Tools_Layout_Abilities` — `includes/abilities/class-layout-abilities.php`
**Condição de registo:** dentro de `if ( $elementor_active )`, sempre. Mesmo par de
dependências injectadas (`$data`, `$factory`). 9 tools — o grupo com mais ferramentas do
Elementor clássico, cobrindo toda a manipulação estrutural da árvore de elementos.
| Tool | Input (resumo) | O que faz | `permission_callback` | readonly / destructive |
|---|---|---|---|---|
| `add-container` | `post_id*`, `parent_id` (vazio = topo), `position` (-1=append), `settings` (flex/grid completo), `full_bleed` (bool) | Cria um `container` via `EMCP_Tools_Element_Factory::create_container()` e insere na árvore. `full_bleed=true` faz merge do preset `full_bleed_preset()` (content_width=full, width 100%, padding/gap zero, column+stretch) **antes** de aplicar `settings` do chamador (que sempre ganham). **Bloqueia** com erro accionável se `EMCP_Tools_Atomic_Props::is_container_supported()` for falso (ver "Gotchas" abaixo). | `check_edit_permission` | não-readonly, não-destructive, não-idempotente |
| `update-container` | `post_id*`, `element_id*`, `settings*` (merge parcial) | Valida que o elemento alvo é mesmo um container (`is_container_type()`) antes de aplicar `update_element_settings()` — devolve erro `not_container` se for widget (aponta para `update-widget`). | `check_edit_permission` | não-readonly, não-destructive, idempotente |
| `update-element` | `post_id*`, `element_id*`, `settings*` | Versão **universal** de update — funciona em qualquer elType, container ou widget, sem o caller precisar de saber qual é. Aceita também `styles`/`editor_settings` no payload (roteados para a raiz do elemento pela camada de dados — ver §Elementor_Data). Recomendado como default em vez de `update-container`/`update-widget` separados. | `check_edit_permission` | não-readonly, não-destructive, idempotente |
| `batch-update` | `post_id*`, `operations*` (array de `{element_id, settings}`) | Aplica múltiplos updates **num único save** (uma leitura + uma escrita da página inteira) — muito mais eficiente que N chamadas a `update-element`. Continua a processar mesmo com falhas parciais; devolve `{success, updated, failed:[{element_id,reason}]}`. | `check_edit_permission` | não-readonly, não-destructive, idempotente |
| `set-element-label` | `post_id*`, `element_id*`, `title*` | Wrapper de conveniência sobre `update_element_settings()` com `editor_settings.title` — define só o rótulo do Navigator (útil sobretudo em elementos atómicos v4, mas funciona em qualquer elType). | `check_edit_permission` | não-readonly, não-destructive, idempotente |
| `reorder-elements` | `post_id*`, `container_id*`, `element_ids*` (ordem desejada) | Reordena os filhos DIRECTOS de um container. Valida que todos os `element_ids` são de facto filhos directos (erro se não). Filhos existentes não mencionados na lista são acrescentados no fim (preservados, não perdidos). | `check_edit_permission` | não-readonly, não-destructive, idempotente |
| `move-element` | `post_id*`, `element_id*`, `target_parent_id*` (vazio=topo), `position*` | Remove o elemento da posição actual e reinsere no destino — implementado como `remove_element()` + `insert_element()` sequenciais sobre a mesma árvore em memória, um único save no fim. | `check_edit_permission` | não-readonly, não-destructive, idempotente |
| `remove-element` | `post_id*`, `element_id*` | Remove o elemento e **todos os filhos** da árvore. | `check_edit_permission` | não-readonly, **destructive**, idempotente |
| `duplicate-element` | `post_id*`, `element_id*` | Clona profundamente o elemento (`reassign_element_ids()` — novos IDs em toda a subárvore, incluindo remapeamento de classes de estilo locais v4 se aplicável) e insere logo a seguir ao original. | `check_edit_permission` | não-readonly, não-destructive, não-idempotente |
### Gotchas de design documentados no código (Layout)
- **`is_container_type()` inclui os tipos atómicos** (`container`, `e-flexbox`, `e-div-block`) —
não é só legado; `update-container`/`reorder-elements` reconhecem containers v4 também
(comentário no código refere issues #104/#72 como o mesmo problema raiz).
- **`add-container` recusa-se a criar um `container` legado se a experiência "Flexbox
Container" do Elementor estiver desligada** (`EMCP_Tools_Atomic_Props::is_container_supported()`).
Antes desta guarda (issue #111), o elemento era gravado com sucesso mas o Elementor
simplesmente **não o renderiza** em runtime — página fica vazia sem qualquer erro visível ao
agente. Este é o tipo de falha silenciosa mais perigosa do plugin: a tool "funciona" (devolve
`success:true`) mas o resultado visual é nada. A mesma guarda está em `build-page` (ver §10).
- **`full_bleed` preset (#83):** em páginas com template Canvas, os defaults "boxed" do
Elementor deixam faixas brancas nas margens de secções full-width (headers/footers). O preset
resolve isto de forma reutilizável em vez de o agente ter de descobrir os 6 campos certos por
tentativa e erro.
---
## 3. `EMCP_Tools_Widget_Abilities` — `includes/abilities/class-widget-abilities.php`
**Condição de registo:** dentro de `if ( $elementor_active )`. `add-pro-widget` só regista se
`defined('ELEMENTOR_PRO_VERSION')` (gate interno na própria classe, não no registrar). 3 tools.
Construtor recebe `$data`, `$factory`, `$schema_generator` (`EMCP_Tools_Schema_Generator`,
usado por `get-widget-schema` noutra classe P0, não aqui) e `$validator`
(`EMCP_Tools_Settings_Validator`, usado para validar settings contra o schema do widget alvo).
| Tool | Input (resumo) | O que faz | `permission_callback` | readonly / destructive |
|---|---|---|---|---|
| `add-free-widget` | `post_id*`, `parent_id*`, `widget_type*`, `position`, `settings` | Valida tier via `EMCP_Tools_Widget_Catalog::is_pro($widget_type)` — **rejeita** (`wrong_tier`) se o tipo pedido for Pro/Woo. Faz merge dos `defaults` do catálogo (`entry['defaults']`) por baixo do `settings` do chamador (chamador sempre ganha). Delega ao motor comum `execute_add_widget()`. | `check_edit_permission` | não-readonly, não-destructive, não-idempotente |
| `add-pro-widget` | idem `add-free-widget` | Espelho exacto, mas com o tier invertido — rejeita widgets free (aponta para `add-free-widget`). Só registada quando Elementor Pro está activo (gate natural: não faz sentido oferecer a tool se não há widgets Pro para colocar). | `check_edit_permission` | não-readonly, não-destructive, não-idempotente |
| `update-widget` | `post_id*`, `element_id*`, `settings*` | Universal para instâncias de widget já colocadas: encontra o elemento, valida `elType==='widget'` (erro `not_a_widget` caso contrário), faz merge parcial de `settings`. | `check_edit_permission` | não-readonly, não-destructive, idempotente |
### Motor comum: `execute_add_widget()` (privado, partilhado pelas duas tools de inserção)
Passos: (1) valida que `widget_type` existe de facto no registo Elementor
(`Plugin::$instance->widgets_manager->get_widget_types($widget_type)` — erro
`invalid_widget_type` se não), (2) se `settings` não vazio, chama
`EMCP_Tools_Settings_Validator::validate($widget_type, $settings)` (mesmo validador de
`includes/validators/`, mas para controls de widget — distinto do `Element_Validator` que
valida a *forma* estrutural do elemento, ver §Elementor_Data), (3) `factory->create_widget()`,
(4) `data->insert_element()`, (5) `data->save_page_data()`.
**Achado de design:** o catálogo (`EMCP_Tools_Widget_Catalog`, não lido nesta tarefa — pertence
provavelmente ao doc 02 ou doc 10) é a fonte de verdade de tier E de defaults por widget — as
duas tools de inserção são finas camadas de gate+merge sobre um motor único; **não há lógica de
posicionamento/inserção duplicada entre free e pro**.
---
## 4. `EMCP_Tools_Template_Abilities` — `includes/abilities/class-template-abilities.php`
**Condição de registo:** dentro de `if ( $elementor_active )`. `save-as-template` e
`apply-template` sempre registadas; as outras 6 só se `defined('ELEMENTOR_PRO_VERSION')`
(gate interno, `get_ability_names()` e `register()` espelham a mesma condição). 8 tools no
total (2 free + 6 Pro).
| Tool | Tier | Input (resumo) | O que faz | `permission_callback` | readonly / destructive |
|---|---|---|---|---|---|
| `save-as-template` | free | `post_id*`, `element_id` (omitir=página inteira), `title*`, `template_type` (`page`\|`section`\|`container`, default page) | Cria um post `elementor_library` com `_elementor_template_type`, define a taxonomia `elementor_library_type`, grava os elementos (página inteira ou só o elemento indicado) como `_elementor_data` desse novo post-template. | `check_edit_permission` | não-readonly, não-destructive, não-idempotente |
| `apply-template` | free | `post_id*`, `template_id*`, `parent_id`, `position` | Lê o template, `reassign_ids()`, insere na página alvo (dentro de `parent_id` ou ao nível de topo). Devolve `elements_added` (contagem via `count_elements()`). | `check_edit_permission` | não-readonly, não-destructive, não-idempotente |
| `create-elementor-theme-template` | **Pro** | `title*`, `template_type*` (enum: header/footer/single/single-post/single-page/archive/search-results/error-404/loop-item) | Cria post `elementor_library` do tipo indicado, inicializa com `_elementor_data=[]`. | `check_edit_permission` | não-readonly, não-destructive, não-idempotente |
| `set-elementor-template-conditions` | **Pro** | `post_id*`, `conditions*` (array de arrays de partes, ex. `["include","singular","post"]`) | Ver "Gotcha crítico #38" abaixo — usa `save_elementor_conditions()`. | `check_edit_permission` | não-readonly, não-destructive, idempotente |
| `list-dynamic-tags` | **Pro** | `group` (filtro opcional) | Enumera `Plugin::instance()->dynamic_tags->get_tags()`, filtra por grupo se indicado, devolve `{name, title, group, categories}` por tag. | `check_edit_permission` | **readonly**, idempotente |
| `set-dynamic-tag` | **Pro** | `post_id*`, `element_id*`, `setting_key*`, `tag_name*`, `tag_settings` | Constrói o valor `[elementor-tag id="…" name="…" settings="…"]` (formato interno do Elementor, `settings` urlencoded como JSON) e escreve-o em `element.settings.__dynamic__[setting_key]`, tornando essa definição dinâmica. | `check_edit_permission` | não-readonly, não-destructive, idempotente |
| `create-popup` | **Pro** | `title*` | Cria post `elementor_library` tipo `popup`. | `check_edit_permission` | não-readonly, não-destructive, não-idempotente |
| `set-popup-settings` | **Pro** | `post_id*`, `triggers`, `conditions`, `timing` | Grava `_elementor_popup_triggers`/`_elementor_popup_timing` em post meta directamente; `conditions` reutiliza o MESMO `save_elementor_conditions()` seguro que o template (não é um caminho separado). | `check_edit_permission` | não-readonly, não-destructive, idempotente |
### Gotcha crítico documentado (#38) — `save_elementor_conditions()`
Método privado partilhado por `set-elementor-template-conditions` e `set-popup-settings`.
**A abordagem anterior** (`update_post_meta()` + `delete_option()` na cache global
`elementor_pro_theme_builder_conditions`) **invalidava a localização de TODOS os templates**
sem os reconstruir — definir condições num template partia silenciosamente headers/footers não
relacionados até um rebuild completo. A abordagem correcta passa pelo **conditions manager** do
próprio Elementor Pro (`ThemeBuilder::get_conditions_manager()->save_conditions()`), que
regenera a cache correctamente; só cai para escrita directa de meta (sem tocar na cache global)
se o gestor Pro não estiver disponível. **Blueprint:** nunca fazer bypass da API de alto nível
de um plugin de terceiros para "poupar uma chamada" quando essa API mantém uma cache
side-effectful — o preço é corromper estado partilhado fora do escopo da própria operação.
---
## 5. `EMCP_Tools_Global_Abilities` — `includes/abilities/class-global-abilities.php`
**Condição de registo:** dentro de `if ( $elementor_active )`, sempre. Construtor só recebe
`$data` (não usa `$factory`). 2 tools — actuam sobre o **kit activo** do Elementor
(`Plugin::$instance->kits_manager->get_active_kit()`), que é o post que guarda paleta global de
cores e tipografia site-wide.
| Tool | Input (resumo) | O que faz | `permission_callback` | readonly / destructive |
|---|---|---|---|---|
| `update-global-colors` | `colors*` (array de `{_id*, title*, color*}` hex) | Lê `kit_settings['custom_colors']`, faz merge por `_id` (actualiza existentes, acrescenta novos), `kit->update_settings(['custom_colors'=>...])`. Regista snapshot no change-ledger ANTES de escrever (`snapshot_kit_settings()`/`record_kit_change()` — captura `_elementor_page_settings` do kit para rollback). | `check_manage_permission`: `manage_options` | não-readonly, não-destructive, idempotente |
| `update-global-typography` | `typography*` (array de `{_id*, title*, typography_font_family, typography_font_size, typography_font_weight, typography_line_height, typography_letter_spacing}`) | Mesmo padrão merge-por-`_id` sobre `kit_settings['custom_typography']`, mas com **allowlist explícita de chaves** (`allowed_keys`) — qualquer campo fora dessa lista é descartado silenciosamente. Força sempre `typography_typography='custom'` (activa o override; sem isto o Elementor ignora os campos custom). | `check_manage_permission` | não-readonly, não-destructive, idempotente |
**Achado de design — a única classe do doc com change-ledger integrado directamente no fluxo de
escrita** (não via o hook central de `save_page_data()`, porque estas duas tools não passam por
`_elementor_data` nenhuma — escrevem `_elementor_page_settings` do post do kit). `snapshot_kit_settings()`
+ `record_kit_change()` chamam `EMCP_Tools_Change_Recorder::record_meta()` explicitamente antes
do `update_settings()`, porque de outra forma uma mudança de cor/tipografia global — que afecta
**todas as páginas do site simultaneamente** — não teria rollback nenhum via o ledger genérico
(esse só cobre `_elementor_data` de um post individual, ver `class-elementor-data.php`).
**Blueprint:** qualquer mutação "global" que não passe pelo caminho de escrita comum de página
precisa do seu PRÓPRIO ponto de integração com o change-ledger — não é automático.
---
## 6. `EMCP_Tools_Global_Classes_Abilities` — `includes/abilities/class-global-classes-abilities.php`
**Condição de registo:** classe auto-gated — `is_available()` verifica
`class_exists('\Elementor\Modules\GlobalClasses\Global_Classes_Repository')` (Elementor 4.0+).
O registrar só instancia se `class_exists('EMCP_Tools_Global_Classes_Abilities')` E chama
sempre `register()`, que internamente re-verifica `is_available()`. 1 tool, **read-only**.
Resolve a peça mais opaca do sistema de design do Elementor 4.0+: elementos referenciam classes
CSS globais só pelo ID opaco `g-xxxxxxx`; sem esta tool, um agente que lê um elemento vê o ID
mas não sabe o que ele estiliza.
| Tool | Input (resumo) | O que faz | `permission_callback` | readonly / destructive |
|---|---|---|---|---|
| `list-global-classes` | `class_ids` (array opcional; omitir = todas) | `Global_Classes_Repository::make()->all()` → normaliza `Collection`/array, para cada item resolve `{id, label, css}` onde `css` é `flatten_variants()` — um mapa `breakpoint[:state] → {prop:valor}` com os `$$type`-wrapped props desembrulhados via `EMCP_Tools_Atomic_Props::unwrap()`. | `check_read_permission`: `edit_posts` | **readonly**, idempotente |
### Gotcha documentado (#57) — resolução defensiva por item
Cada item é resolvido dentro do seu próprio `try/catch`. **Antes desta guarda**, uma única
classe malformada fazia a enumeração inteira (`resolve-all`, sem `class_ids`) falhar por
completo, enquanto pedidos com `class_ids` explícitos (que saltam a entrada má) continuavam a
funcionar — inconsistência confusa de diagnosticar ("porque é que só falha quando não passo
IDs?"). A correcção devolve a classe problemática mesmo assim, com `css:[]` e um campo `error`
explicativo, em vez de a fazer desaparecer da lista ou abortar tudo. **Blueprint:** ao
enumerar uma colecção de N itens onde cada um pode ter forma inesperada, isolar cada resolução —
nunca deixar um item mau abortar os outros N-1.
---
## 7. `EMCP_Tools_Global_Classes_Write_Abilities` — `includes/abilities/class-global-classes-write-abilities.php`
**Condição de registo:** mesmo padrão auto-gated (`is_available()` delega para a classe de
leitura se existir, senão verifica a mesma constante `REPOSITORY` directamente). 4 tools —
**todas em `emcp_tools_disabled_tools` por omissão** (mutação de CSS partilhado entre todas as
páginas do site é tratada como categoria de alto risco, ver `skill://emcp-tools` §2.9/§5).
Permissão de escrita: `elementor_global_classes_update_class` (capability própria do Elementor,
tipicamente só admin) OU `manage_options`.
| Tool | Input (resumo) | O que faz | Destructive |
|---|---|---|---|
| `create-global-class` | `label*`, `styles` (mapa amigável), `props` (escape-hatch raw `$$type`), `breakpoint` (enum de 7 valores, default desktop), `state` (opcional: hover/focus/…) | Lê o estado actual (`items`, `order`) via `read_state()`, gera um novo ID (`mint_id()` — `g-` + 4 bytes hex aleatórios, sem colisão), constrói o objecto `{id, type:class, label, variants:[{meta, props}]}` combinando `styles` (traduzido via `EMCP_Tools_Atomic_Styles::build_common_props()`+`build_flex_props()`) com `props` raw por cima, escreve com `write_state()`. | não |
| `update-global-class` | `id*`, `label`, `styles`, `props`, `breakpoint`, `state`, `replace_variant` (bool) | Localiza a variante pelo par `(breakpoint, state)` (`find_variant_index()`); se não existir, acrescenta uma nova variante; se existir e `replace_variant=false` (default), faz merge dos props na variante existente; se `true`, substitui-a inteira. | não |
| `delete-global-class` | `id*`, `confirm*` (deve ser `true`) | Remove do mapa `items` e da lista `order`; **exige `confirm:true`** explicitamente — é a única das 4 a ter esse requisito extra, porque apaga a classe de TODOS os elementos que a usam. | **sim** |
| `reorder-global-classes` | `order*` (array de IDs `g-`) | A ordem do Class Manager É a ordem de saída CSS — decide qual classe ganha quando duas se aplicam ao mesmo elemento com a mesma especificidade. IDs omitidos em `order` são acrescentados no fim, na ordem actual — **nenhuma classe pode desaparecer** por um reorder parcial (a "baseline order" é a união de `current_order` + `array_keys(items)`, nunca só o que o chamador mandou). | não |
### Como as escritas persistem — o padrão `read_state()`/`write_state()`
Todas as 4 tools passam pelo repositório oficial do Elementor
(`Global_Classes_Repository::make()`), nunca por meta directa: lê o mapa completo `id => item`
+ `order[]`, muta em memória, chama `put($items, $order)` — **o Elementor calcula o diff
add/modify/delete internamente** e trata relações + limpeza de uso. `write_state()` faz
best-effort de espelhar também para o contexto de preview (`set_preview(true)->put(...)`) — se
esse segundo write falhar, é tolerado silenciosamente (só logado com `WP_DEBUG`) porque **o
write de frontend é a fonte de verdade**; o preview só afecta o que o editor mostra até
recarregar.
**Achado de design — reutilização directa dos tijolos atómicos v4:** `build_variant_props()`
chama `EMCP_Tools_Atomic_Styles::build_common_props()`/`build_flex_props()` — as MESMAS classes
de suporte que o sistema de widgets atómicos (doc 02) usa para construir estilos locais por
elemento. Isto significa que "escrever uma Global Class" e "aplicar um estilo local a um
elemento atómico" partilham o mesmo motor de tradução `styles amigável → props $$type-wrapped`
— não há dois formatos de estilo diferentes no plugin, só dois destinos de armazenamento
(classe global partilhada vs. classe local de um elemento).
---
## 8. `EMCP_Tools_Custom_Code_Abilities` — `includes/abilities/class-custom-code-abilities.php`
**Condição de registo:** dentro de `if ( $elementor_active )`. `add-custom-js` sempre regista
(funciona com Elementor free, via widget HTML); as outras 3 só se `defined('ELEMENTOR_PRO_VERSION')`.
4 tools no total (1 free + 3 Pro). Injecção de código executável — a classe com o perfil de
risco mais alto deste doc, com o maior número de comentários de segurança no código-fonte.
| Tool | Tier | Input (resumo) | O que faz | `permission_callback` | Destructive |
|---|---|---|---|---|---|
| `add-custom-css` | **Pro** | `post_id*`, `element_id` (omitir=nível de página), `css*`, `replace` (bool) | CSS por elemento usa o placeholder `selector` como wrapper (substituído pelo Elementor no seu gerador de CSS); grava em `settings.custom_css` do elemento ou em `page_settings.custom_css`. Por omissão faz *append*; `replace=true` sobrescreve. Sanitização: remove tags PHP e `<script>`, e **neutraliza `</style>` em loop até fixpoint** (ver F-004 abaixo). | `check_edit_permission` | não |
| `add-custom-js` | free | `post_id*`, `parent_id*`, `js*`, `position`, `wrap_dom_ready` (bool) | Insere um **widget HTML** contendo `<script>{js}</script>` na árvore da página (não é injecção site-wide, é conteúdo normal da página). Remove qualquer `<script>`/`</script>` que o chamador já tenha incluído (evita duplo-wrap); opcionalmente envolve em `DOMContentLoaded`. | `check_js_permission`: `edit_posts`+per-post **E** `unfiltered_html` — a única tool do grupo a exigir `unfiltered_html` além da capability de edição normal, porque injecta um `<script>` executável que o WordPress tiraria a um utilizador sem essa capability (ex. não-super-admin em multisite) | não |
| `add-code-snippet` | **Pro** | `title*`, `code*`, `location` (`head`\|`body_start`\|`body_end`, default head), `priority` (1-10, clamp), `status` (`publish`\|`draft`), `ensure_jquery` (bool) | Cria um post CPT `elementor_snippet` com meta `_elementor_location`/`_elementor_priority`/`_elementor_code`/`_elementor_template_type=code_snippet` — **injecção site-wide**, em TODAS as páginas, ao contrário de `add-custom-js` (só naquela página). | `check_snippet_permission`: `manage_options` **E** `unfiltered_html` | não |
| `list-code-snippets` | **Pro** | `location` (filtro), `status` (default `any`) | Lista posts `elementor_snippet` (até 100), devolve `{id, title, location, priority, status, code, edit_url}` por snippet. | `check_manage_permission`: `manage_options` | readonly |
### Gotchas de segurança documentados no código (Custom Code)
- **F-004 — bypass de `</style>` neutralizado em loop até fixpoint:** o CSS de `add-custom-css`
é emitido dentro de um bloco `<style>`, que o parser HTML trata como texto bruto — a ÚNICA
forma de escapar para HTML vivo (vector XSS, ex. `</style><img onerror=...>`) é a tag literal
`</style>`; `<`, `>` isolados ou até `<img>` sozinhos são inertes sem ela. O código remove
`</\s*style` **num `while` até `$previous === $css`**, especificamente para impedir que
remover UMA ocorrência reconstrua outra por concatenação adjacente. Importante: preserva TODA
a CSS válida — combinadores `>`/`~`/`+`, media queries com `<`/`>`, strings de conteúdo — só a
sequência exacta `</style` desaparece.
- **F-008 — regex de handlers `on*=` precisa da flag `/s` (DOTALL):** em `sanitize_svg_content()`
(classe SVG, não esta, mas o padrão de regex é idêntico e vale a pena registar aqui porque
`add-custom-css` faz sanitização de string semelhante) — sem `/s`, um handler cujo VALOR contém
uma quebra de linha (`onclick="alert(1)\n"`) escapa ao match porque `.` por omissão não cruza
linhas.
- **Distinção free vs Pro não é arbitrária:** `add-custom-js` (free) é sempre **por-página** (um
widget HTML normal, mesma superfície de risco que qualquer conteúdo de página); os 3 Pro
operam **site-wide** — `add-custom-css` a nível de página TAMBÉM é possível mas
`add-code-snippet` injecta sempre em todas as páginas. É essa amplitude, não a linguagem em
si, que justifica o gate `manage_options` (site-wide) vs `edit_posts` (por-página).
---
## 9. `EMCP_Tools_Svg_Icon_Abilities` — `includes/abilities/class-svg-icon-abilities.php`
**Condição de registo:** dentro de `if ( $elementor_active )`, sempre — mas na prática a tool
não depende de nada específico do Elementor além do formato de saída (o objecto ícone). 1 tool.
| Tool | Input (resumo) | O que faz | `permission_callback` | readonly / destructive |
|---|---|---|---|---|
| `upload-svg-icon` | `svg_url` OU `svg_content` (mutuamente exclusivos), `title` | Faz upload/sideload de um SVG para a Media Library e devolve o objecto de ícone Elementor pronto a usar: `{value:{id,url}, library:'svg'}` — directamente atribuível a `selected_icon` em qualquer widget icon/icon-box/button. | `check_upload_permission`: `upload_files` | não-readonly, não-destructive, não-idempotente |
### Pipeline de segurança (fail-closed em 3 camadas)
1. **Download (`svg_url`):** via `EMCP_Tools_Url_Guard::safe_download()` — guarda SSRF que
bloqueia hosts privados/reservados/loopback e **revalida cada hop de redirect** (não é lido
nesta tarefa, mas o nome da classe/comportamento fica documentado como dependência crítica).
2. **Bypass temporário do filtro de MIME do WordPress:** dois filtros temporários
(`upload_mimes`, `wp_check_filetype_and_ext`) permitem o `.svg` só durante a chamada e são
removidos logo a seguir — não altera a política global de uploads do site.
3. **Sanitização fail-closed, dupla camada:**
- Verificação superficial: rejeita se contiver `<script` (regra própria, antes de chamar o
sanitizador).
- Sanitizador real: `\Elementor\Core\Utils\Svg\Svg_Sanitizer` (classe do PRÓPRIO Elementor,
reaproveitada — não é uma dependência própria do EMCP Tools). **Se a classe do sanitizador
não existir, a tool recusa o upload por completo** (`no_svg_sanitizer`) em vez de aceitar
markup minimamente verificado — fail-closed genuíno, não um "melhor esforço".
- Para `svg_content`: além do sanitizador, regex adicionais removem handlers `on*=` (com
`/s` — ver F-008 acima) e URLs `javascript:`.
**Blueprint:** este é o padrão de referência para QUALQUER tool de upload de conteúdo
potencialmente executável (SVG pode conter script) — nunca confiar só na extensão de ficheiro
nem só num sanitizador; usar o MELHOR sanitizador disponível (aqui, reaproveitar o do Elementor
em vez de reescrever um) e recusar em vez de degradar quando ele não está presente.
---
## 10. `EMCP_Tools_Composite_Abilities` — `includes/abilities/class-composite-abilities.php`
**Condição de registo:** dentro de `if ( $elementor_active )`, sempre. 1 tool — mas é a mais
sofisticada do doc: constrói uma página inteira a partir de uma especificação declarativa
aninhada, numa única chamada MCP.
| Tool | Input (resumo) | O que faz | `permission_callback` | readonly / destructive |
|---|---|---|---|---|
| `build-page` | `title*`, `status` (default draft), `post_type` (default page), `page_settings`, `dry_run` (bool), `structure*` (árvore declarativa: `{type, widget_type?, settings?, children?}[]`) | Constrói recursivamente a árvore Elementor a partir de `structure` (em memória, sem write ainda), cria o post e grava só se `dry_run` não estiver activo. | `check_create_permission`: `publish_pages` \|\| `edit_pages` | não-readonly, não-destructive, não-idempotente |
### Passos de execução (`execute_build_page`)
1. **Guarda de suporte de container** (idêntica à de `add-container`, ver §2) — recusa
antecipadamente se a experiência Flexbox Container estiver desligada, porque `build-page`
emite `container`s legados que renderizariam vazios (#111).
2. **`build_elements()` recursivo** — percorre `structure`, normaliza cada nó
(`normalize_node()`, ver abaixo), constrói `container`s ou widgets, acumula
`$this->elements_created` e `$this->warnings`.
3. Se `elements_created > 150` (`SOFT_ELEMENT_LIMIT`), acrescenta um aviso sobre risco de
timeout num conector MCP remoto — sugere `dry_run` primeiro ou dividir em várias chamadas.
4. **`dry_run=true`:** devolve `{dry_run:true, would_create:N, warnings:[...]}` **sem tocar na
base de dados** — nenhum post é criado.
5. Caso contrário: `wp_insert_post()`, `save_page_data()`, `save_page_settings()` (se
fornecido), devolve `post_id`/`edit_url`/`preview_url`/`elements_created`/`warnings`.
### `normalize_node()` — coerção tolerante de shorthand de modelos fracos
Modelos de IA mais fracos escrevem rotineiramente `{"type":"heading", ...}` em vez da forma
canónica `{"type":"widget","widget_type":"heading"}`, ou dão a um container um `type` diferente
de `"container"` mas ainda com `children`. Em vez de descartar silenciosamente estes nós (o que
faria o pedido "ter sucesso" com colunas vazias no resultado — o pior tipo de falha, porque
parece funcionar), `normalize_node()`:
- Se o nó tem `children` não-vazio → tratado como container, independentemente do `type`
declarado; se o `type` original não era `"container"`, regista um warning.
- Se não tem `children` e tem `type` não-vazio → interpretado como **shorthand de widget**: o
próprio `type` torna-se `widget_type` (ex. `"heading"` → widget heading); warning explica a
forma preferida.
- Devolve sempre o nó com `type` canónico (`container`/`widget`), nunca lança erro — a
filosofia é "aceitar o que o modelo quis dizer, mas dizer exactamente o que foi assumido".
### Layout automático em containers `flex_direction=row`
Quando um container pai tem `flex_direction=row` (ou `row-reverse`) e mais de 1 filho:
- Containers filhos SEM largura explícita (`width`/`_flex_size`/`_flex_grow` já definidos)
recebem `content_width=full` + `width={size: 100/N, unit:'%'}` automaticamente — replica o
padrão nativo de colunas do Elementor sem o agente ter de calcular percentagens.
- **Widgets colocados directamente como filho de um row** (sem container intermédio) são
**automaticamente envolvidos** num container-coluna com a mesma largura calculada — porque o
modelo flex do Elementor exige um container como flex-item; um widget "nu" não tem
flex-basis e simplesmente esticaria para preencher a row em vez de formar uma coluna própria.
Isto acrescenta um elemento extra à árvore (contabilizado em `elements_created`) que o
chamador não pediu explicitamente, mas sem o qual o layout pedido (colunas lado-a-lado) nem
sequer seria possível.
- **Explicitamente PROIBIDO no schema** (bloco `description` da tool): nunca definir `flex_wrap`
ou `_flex_size` manualmente — a tool já trata disto e sobreposições manuais causam overflow de
layout.
### `build_widget()` — ponte para widgets atómicos v4 dentro de `build-page`
Se `EMCP_Tools_Atomic_Widget_Map::is_atomic($widget_type)` for verdadeiro, `build-page` **não**
usa `factory->create_widget()` legado — usa `factory->create_atomic_widget()` com os settings
mapeados por `EMCP_Tools_Atomic_Widget_Map::settings()` (o mesmo mapeamento que as tools
`add-atomic-*` individuais usam, doc 02), e aplica os parâmetros de estilo restantes como uma
classe local via `EMCP_Tools_Atomic_Styles::create_local_class()`. Isto significa que
**`build-page` é atomic-aware por composição**, não por duplicação — reaproveita inteiramente o
motor do doc 02 em vez de ter a sua própria lógica de tradução de props atómicas.
**Blueprint:** `build-page` é o exemplo mais claro no plugin de "tool de alto nível como
orquestrador fino sobre primitivas de baixo nível" — não introduz nenhuma capacidade de
persistência nova, só composição declarativa + tolerância a input ambíguo em cima de
`Element_Factory`+`Data`(+`Atomic_*` quando aplicável). Uma réplica deveria construir esta tool
POR ÚLTIMO, depois de todas as primitivas (container/widget/atomic) já funcionarem
individualmente.
---
## Serviços de suporte
### `EMCP_Tools_Element_Factory` — `includes/class-element-factory.php`
Fábrica pura (sem I/O, sem WordPress DB) que constrói arrays PHP no formato exacto que o
Elementor espera para cada tipo de elemento. Métodos: `create_container()`, `create_widget()`,
`create_section()`/`create_column()` (legado pré-Container, usado só residualmente — nenhuma
tool destas 10 classes os invoca directamente, mantidos por compatibilidade), e os 3 atómicos
`create_atomic_widget()`/`create_flexbox()`/`create_div_block()` (Elementor 4.0+, cobertos em
detalhe no doc 02 mas usados aqui indirectamente por `build-page`).
**Duas normalizações estáticas reutilizadas em toda a base de código** (chamadas não só pela
factory mas também por `EMCP_Tools_Data::update_element_settings()`):
- **`normalize_container_settings()`** — remapeia os atalhos sem prefixo `justify_content` /
`align_items` / `align_content` para as chaves prefixadas `flex_justify_content` /
`flex_align_items` / `flex_align_content` que o schema de container do Elementor **realmente
lê**. **Gotcha crítico (#32):** sem este remap, os valores eram persistidos sob nomes que o
gerador de CSS do Elementor nunca consulta — as custom properties CSS (`--justify-content`,
`--align-items`) nunca são emitidas e o container renderiza com alinhamento default no
frontend, apesar dos dados estarem "correctos" na base de dados. Chaves prefixadas fornecidas
pelo chamador sempre ganham sobre o atalho, se ambas aparecerem no mesmo payload.
- **`normalize_background_settings()`** — corrige 3 formas erradas-mas-intuitivas de background
que modelos fracos emitem: (1) um grupo aninhado `background: {background_image, size, ...}`
é achatado para chaves `background_*` de topo (o Elementor não tem control de grupo
`background`, um objecto aninhado é simplesmente ignorado); (2) `background_image` dado como
array de objectos `[{id,url}]` (o modelo espelha a forma de um media-repeater) é desembrulhado
para o objecto único `{id,url}` esperado; (3) quando existe imagem OU cor mas falta o
activador `background_background`, injecta `classic` automaticamente — sem o activador o
Elementor nunca renderiza background nenhum. Idempotente e não-destrutivo: chaves planas já
fornecidas pelo chamador sempre ganham sobre o que é elevado do grupo aninhado.
Container `create_container()` também aplica um default de UX: **auto-centra
`flex_align_items='center'`** em containers coluna não-grid quando o chamador não especificou
alinhamento — só linhas (`row`) ficam com o comportamento default do Elementor.
### `EMCP_Tools_Data` — `includes/class-elementor-data.php`
**A camada de leitura/escrita real do `_elementor_data`** — usada por praticamente todas as 10
classes deste doc (excepto Global Classes, que usa o repositório próprio do Elementor
directamente, e Global_Abilities, que usa o kit manager). ~700 linhas; o ficheiro mais denso em
comentários de bug-fix real de todo o doc. Métodos-chave:
- **`get_document()`** — obtém o `\Elementor\Core\Base\Document` para um post via
`Plugin::$instance->documents->get($post_id)`. Guardado por `elementor_documents_ready()`:
o gestor de documentos do Elementor só existe depois do seu próprio hook `init` correr; durante
a ACTIVAÇÃO do Elementor (que insere o kit por omissão via `save_post`, o que dispara o
indexador do EMCP Tools) essa dependência ainda não existe — sem a guarda seria um fatal
error por null-deref (#105).
- **`get_page_data()`** — tenta primeiro `$document->get_elements_data()`; se vazio, cai para
leitura directa de `_elementor_data` (post meta bruto, `json_decode`). O fallback existe
porque em contexto CLI/proxy (sem browser, sessão de editor) o API do documento por vezes
devolve vazio mesmo com dados presentes na base de dados.
- **`save_page_data()` — o método mais complexo de todo o ficheiro (~140 linhas).** Fluxo
completo:
1. **`EMCP_Tools_Atomic_Props::coerce_tree($data)`** — varre a árvore INTEIRA (não só o
elemento a alterar) antes de gravar. **Gotcha #102:** uma versão anterior só coagia o
elemento sendo escrito; como o Elementor 4.x valida a ÁRVORE COMPLETA no save, um único
widget com um valor de prop bruto (não `$$type`-wrapped) noutro sítio da página bloqueava
TODOS os saves futuros — incluindo o save destinado a reparar esse mesmo widget. Fazer a
coerção ser sempre sobre a árvore inteira é um no-op para páginas saudáveis e uma rede de
segurança universal para páginas com dados legados/corrompidos.
2. **Preserva `_elementor_data` corrupto** antes de sobrescrever — se a meta actual for uma
string não-vazia que não faz `json_decode` válido, é copiada para
`_elementor_data_emcp_corrupt` antes do save prosseguir. Sem isto, `get_page_data()` trata
"corrupto" como "vazio" e um save subsequente apagaria os dados originais para sempre.
3. **`try { $document->save(...) } catch (\Throwable $e)`** — o Elementor 4.x atómico
**lança excepção** (não devolve `false`) quando a validação de settings/estilos falha.
`is_atomic_validation_rejection()` distingue uma rejeição de validação legítima (mensagem
contém "validation failed" ou "invalid_value") de um erro fatal genuíno. **Gotcha #112:**
um prop `{$$type:'dynamic'}` gravado pelo editor ao vivo pode referenciar uma dynamic tag
que o registo atómico em contexto CLI/REST não consegue resolver — e como a validação é
sobre a árvore inteira, isso bloquearia QUALQUER save da página, incluindo edições a
elementos totalmente não relacionados. Uma rejeição de validação é tratada como "dados
legítimos que este contexto não sabe verificar" e roteada para o fallback de meta directa
em vez de reprovada.
4. **Verificação pós-save (#98):** mesmo quando `$document->save()` devolve verdadeiro sem
excepção, relê `_elementor_data` e confirma que os dados enviados realmente persistiram —
em certos contextos 4.x/atómicos/REST o save pode devolver "sucesso" e ainda assim
esvaziar `_elementor_data`. Se detectado, força o mesmo fallback de escrita directa em vez
de reportar um sucesso fantasma ao chamador.
5. **Fallback de meta directa:** `update_post_meta('_elementor_data', wp_slash(json_encode($data)))`
+ garante `_elementor_edit_mode=builder` + `_elementor_version` + invalida cache CSS
(`delete_post_meta('_elementor_css')` + apaga o ficheiro físico
`uploads/elementor/css/post-{id}.css` se existir) + invalida a cache de elemento renderizado
do Elementor 4.2 (`_elementor_element_cache`, ver hook `init()` abaixo).
6. **Regista no change-ledger** — `EMCP_Tools_Change_Recorder::record_elementor()` (ou
fallback directo a `EMCP_Tools_Change_Log::record()`), capturando o `_elementor_data`
ANTERIOR completo para permitir rollback via `rollback-change` (doc 05).
- **`init()` (hook estático global)** — regista em `added_post_meta`/`updated_post_meta`: sempre
que `_elementor_data` é escrito, por QUALQUER caminho (as nossas tools, o editor, um import),
apaga `_elementor_element_cache`. **Motivo (#111 revisitado):** o Elementor 4.2 introduziu uma
cache de HTML renderizado nessa meta key; o próprio Elementor limpa-a em `Document::save()`,
mas o fallback de meta directa do EMCP Tools bypassa isso. Numa instalação com object cache
persistente (ex. WP Engine), uma entrada vazia/obsoleta sobrevivia a QUALQUER escrita de
conteúdo subsequente — uma página criada via MCP (escrita enquanto os dados ainda eram `[]`,
renderizada [caching vazio], depois preenchida) servia o render vazio em cache para sempre.
Este hook restaura a invalidação universalmente para qualquer caminho de escrita.
- **`insert_element()` / `remove_element()` / `reassign_ids()` / `reassign_element_ids()` /
`count_elements()` / `find_element_by_id()`** — utilitários recursivos puros sobre a árvore em
memória (todos operam por referência `&$data` onde relevante para evitar cópias de arrays
grandes a cada nível de recursão). `reassign_element_ids()` também chama
`EMCP_Tools_Atomic_Styles::remap_local_classes()` — **gotcha #97:** classes de estilo locais
v4 (`e-<id>-<hash>`) pertencem a UM elemento; duplicar um elemento sem remapear as suas classes
locais fazia o duplicado partilhar as classes do original — vazamento de estilo entre
elementos e duplicação da "Origem de Estilo" no editor.
- **`update_element_settings()` — o segundo método mais complexo.** Além do merge óbvio de
`settings`, faz:
- **Hoist de chaves-irmãs da raiz** (`styles`, `editor_settings`) — em elementos atómicos v4,
o mapa `styles` local e `editor_settings` (rótulo Navigator) vivem na RAIZ do elemento, como
irmãos de `settings`, não dentro dele. Um agente naturalmente aninha-os sob `settings`;
`update_element_settings()` intercepta essas duas chaves ANTES do merge normal, remove-as do
payload de settings, e faz `deep_merge()` para a raiz do elemento (**gotcha #72/#73** — sem
isto, eram gravadas em `settings.styles`, uma chave morta que o Elementor nunca lê).
- **Normalização condicional por tipo:** containers passam por
`normalize_container_settings()`; qualquer outro elType passa só por
`normalize_background_settings()` (mesma correcção de background, sem o remap de flex que
só faz sentido em containers).
- **`EMCP_Tools_Atomic_Props::coerce_settings()` no settings MERGED** (não só no incoming) para
widgets — **gotcha #101:** um valor bruto (`'Hello'` em vez de
`{$$type:'html-v3',value:'Hello'}`) em prop atómico não é simplesmente ignorado — "envenena"
o elemento: o Elementor cai para o default do prop (renderiza texto placeholder) E todo o
save subsequente da página lança "Settings validation failed", trancando a página fora tanto
da API como do próprio editor visual. Correr a coerção sobre o resultado do merge aceita
valores simples que um agente naturalmente envia E repara qualquer coisa que uma versão
anterior já tenha gravado incorrectamente.
- **`sync_local_class_refs()` quando `styles` foi tocado — gotcha #92:** uma classe de estilo
local só renderiza se `settings.classes` (o prop `$$type:'classes'` que lista IDs aplicados)
referenciar o seu ID. Um agente que escreve um mapa `styles` mas esquece de acrescentar o ID
a `classes` obtém um no-op silencioso — o estilo persiste na base de dados mas nunca se
aplica visualmente. Este método varre `item['styles']` (só entradas `type==='class'`) e
garante que todos os IDs aparecem em `settings.classes.value`, idempotente.
- **`deep_merge()`** — merge recursivo próprio: mapas associativos fazem merge chave-a-chave;
listas (arrays numéricos sequenciais, ex. um array `variants`) e escalares são **substituídos
por inteiro** pelo valor incoming. Permite que um update parcial de `styles`/`editor_settings`
toque só uma classe/chave sem apagar as irmãs, mantendo ao mesmo tempo a substituição total
quando o chamador manda de facto uma lista nova completa.
### `EMCP_Tools_Element_Validator` — `includes/validators/class-element-validator.php`
Classe pequena e propositadamente simples (~55 linhas) — valida só a **forma estrutural** de um
elemento isolado antes de ser gravado: `id` presente, `elType` presente e num allowlist fixo
(`container`, `widget`, `section`, `column`, mais os tipos atómicos `e-div-block`, `e-flexbox`,
mais os tipos de estrutura de formulário atómico `e-tabs*`/`e-form*`), e `widgetType` presente
quando `elType==='widget'`. **Não é chamada por nenhuma das 10 classes deste doc directamente**
(não aparece em nenhuma das leituras de `add-container`/`add-*-widget`/`build-page`) — é
provavelmente invocada num caminho de import/validação mais genérico não coberto por esta
tarefa (possivelmente `import-sandbox-artifact` ou o dispatcher de validação de widgets custom
do doc 06, dado o allowlist incluir tipos `e-form-*` que não aparecem em mais nenhum ficheiro
lido nesta tarefa). Não confundir com `EMCP_Tools_Settings_Validator` (validador de VALORES de
settings contra o schema de controls de um widget — usado por `Widget_Abilities::execute_add_widget()`,
ver §3) nem com `EMCP_Tools_Atomic_Props` (coerção/validação de tipos de prop atómico v4 — usado
extensivamente por `EMCP_Tools_Data`, coberto em detalhe no doc 02).
---
## Blueprint para réplica
### Copiar quase 1:1 (baixo risco de reescrever pior)
- **`EMCP_Tools_Element_Factory`** — fábrica pura, sem I/O; as duas normalizações estáticas
(`normalize_container_settings`, `normalize_background_settings`) codificam conhecimento
tácito real sobre onde o Elementor lê cada chave (não documentado nem no Elementor nem em
lado nenhum público) — recriá-las do zero significa redescobrir os mesmos 3-4 bugs (#32 em
particular) por tentativa e erro num site de produção.
- **O padrão try/save/verify/fallback de `save_page_data()`** — é o resultado de pelo menos 5
issues reais numeradas (#98, #101, #102, #105, #111, #112) resolvidas ao longo de várias
versões. Uma réplica que escreva `_elementor_data` só com `update_post_meta()` direto (sem
passar primeiro pelo `Document::save()` nativo) perde a regeneração de CSS automática do
Elementor; uma que só use `Document::save()` sem fallback nem verificação pós-save vai falhar
silenciosamente em contexto CLI/REST exactamente como a v1.0.0 original deste plugin
presumivelmente fazia antes destas correcções serem adicionadas.
- **O hook `init()` de invalidação de `_elementor_element_cache`** — 4 linhas de código que
previnem uma classe inteira de bugs "página fica vazia depois de criada via MCP" em sites com
object cache persistente. Trivial de replicar, caro de não ter.
- **O padrão de `Global_Classes_Write_Abilities`: read-mutate-put via o repositório oficial do
Elementor** em vez de escrita directa de meta — delega o cálculo de diff e limpeza de relações
ao próprio Elementor. Reescrever isto por fora (calcular o diff manualmente) só faz sentido se
se estiver a substituir inteiramente o sistema de Class Manager, não a interagir com ele.
### Simplificar na reescrita
- **Os 3 tools Pro de `Custom_Code_Abilities`** (`add-custom-css`, `add-code-snippet`,
`list-code-snippets`) dependem de comportamento interno específico do Elementor Pro (CPT
`elementor_snippet`, meta keys `_elementor_location`/`_elementor_priority`/`_elementor_code`).
Numa réplica sem o objectivo de espelhar exactamente o Elementor Pro, um sistema de snippets
site-wide próprio (CPT nosso, sem tentar imitar o formato do Elementor Pro) é mais simples de
manter e não fica preso a mudanças não documentadas do formato interno de um plugin de
terceiros.
- **Os 6 tools Pro de `Template_Abilities`** (theme templates, dynamic tags, popups) só fazem
sentido se se estiver mesmo a espelhar o Elementor Pro Theme Builder; se a réplica não visa
paridade total com Elementor Pro, este bloco inteiro pode ficar de fora sem perda de valor
para o caso de uso "gerar/editar páginas com Elementor free".
- **`normalize_node()` (shorthand coercion em `build-page`)** — é uma correcção pragmática para
modelos de IA fracos, não uma necessidade estrutural. Numa réplica visada a modelos fortes
(ex. só Claude/GPT-4 classe), pode-se optar por rejeitar shorthand com erro claro em vez de
coagir silenciosamente — troca tolerância por previsibilidade; ambas são escolhas válidas,
mas a escolha deve ser deliberada, não copiada por omissão.
### Riscos/gotchas a não esquecer (lista consolidada, por nº de issue)
| # | Onde | Risco se ignorado numa réplica |
|---|---|---|
| #32 | `normalize_container_settings` | Atalhos `justify_content`/`align_items` gravados mas nunca lidos pelo gerador CSS — alinhamento nunca aplica no frontend |
| #38 | `save_elementor_conditions` | Bypass da API de condições do Theme Builder invalida location cache de TODOS os templates, não só o alterado |
| #72/#73/#92 | `update_element_settings` (styles/editor_settings hoist + sync_local_class_refs) | Estilos locais v4 gravados mas nunca aplicados; rótulo Navigator gravado em chave morta |
| #83 | `full_bleed_preset` | Faixas brancas em templates Canvas com secções full-width |
| #97 | `reassign_element_ids` | Duplicar um elemento atómico faz o duplicado herdar (e poluir) as classes locais do original |
| #98 | `save_page_data` verificação pós-save | `Document::save()` pode devolver sucesso e mesmo assim não persistir nada em contexto 4.x/REST |
| #101/#102 | `coerce_settings`/`coerce_tree` | Um valor bruto num prop atómico tranca TODA a página fora de futuros saves, não só o elemento afectado |
| #104/#72(layout) | `is_container_type` | Ferramentas de layout que só reconhecem `container` legado ignoram containers atómicos v4 (`e-flexbox`/`e-div-block`) |
| #105 | `elementor_documents_ready` | Fatal error por null-deref se o Elementor ainda não completou o próprio boot |
| #108 | `Global_Classes_Write_Abilities` (comentário de cabeçalho) | Tools de escrita de Global Classes são recentes (3.9.0) — API do Elementor para isto é jovem, sujeita a mudança |
| #111 | `is_container_supported` (2 sítios: add-container e build-page) | Container gravado com sucesso mas invisível no frontend se a experiência Flexbox Container estiver desligada — falha totalmente silenciosa |
| #112 | `is_atomic_validation_rejection` | Uma dynamic tag não resolvível em contexto CLI bloqueia save de página inteira, incluindo edições não relacionadas |
| F-004 | `add-custom-css` sanitização | Bypass de `</style>` como vector XSS se a remoção não for feita em loop até fixpoint |
| F-008 | Regex de handlers `on*=` (SVG + custom CSS) | Handler com valor multi-linha escapa à sanitização sem a flag `/s` |
---
## Fonte
Leitura directa (19-08-2026) de, sob
`/home/ealmeida/emanuelalmeida.pt/wp-content/plugins/emcp-tools/`:
- `includes/abilities/class-page-abilities.php`
- `includes/abilities/class-layout-abilities.php`
- `includes/abilities/class-widget-abilities.php`
- `includes/abilities/class-template-abilities.php`
- `includes/abilities/class-global-abilities.php`
- `includes/abilities/class-global-classes-abilities.php`
- `includes/abilities/class-global-classes-write-abilities.php`
- `includes/abilities/class-custom-code-abilities.php`
- `includes/abilities/class-svg-icon-abilities.php`
- `includes/abilities/class-composite-abilities.php`
- `includes/class-element-factory.php`
- `includes/class-elementor-data.php`
- `includes/validators/class-element-validator.php`
- `includes/abilities/class-ability-registrar.php` (linhas 430-530, só para confirmar a condição
de registo — `if ( $elementor_active )` — e a ausência de qualquer classe de CRUD de widget
custom neste bloco)
Cruzado com `docs/00-ARQUITECTURA.md` (mesma tarefa, doc irmão) para o contrato de
`emcp_tools_register_ability()` e a ordem de arranque, e com `skill://emcp-tools` (auditoria de
segurança, 16-08-2026) para a lista de tools deste grupo presentes no deny-list por omissão
(§2.6/§2.9/§2.10 desse documento).