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

56 KiB

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 containers legados que renderizariam vazios (#111).
  2. build_elements() recursivo — percorre structure, normaliza cada nó (normalize_node(), ver abaixo), constrói containers 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).