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).
633 lines
56 KiB
Markdown
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).
|