Files
emcp-tools-mapping/docs/03-WORDPRESS-CORE-TEMAS.md
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

495 lines
44 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 03 — WordPress core (conteúdo/media/settings/plugins/temas/users/menus) e integrações de temas/frameworks de blocos
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. Ver `docs/00-ARQUITECTURA.md` para o contrato `emcp_tools_register_ability()` e o
padrão de arranque; este documento assume esse contexto e não o repete.
Este grupo cobre **duas famílias distintas** de abilities:
1. **WordPress core puro** (content, media, settings, plugins, themes, users, nav menus) — sempre
activas, sem dependência de Elementor nem de nenhum framework de terceiros.
2. **Integrações de temas/frameworks de blocos** (Astra, Spectra, Kadence, Kadence Blocks) — o
padrão "Themes tab" de dois dispatchers (`<id>-read`/`<id>-write`) por integração, cada uma
condicional à presença do tema/plugin correspondente. Free tem 5 integrações concretas
(Active Theme + Astra + Spectra + Kadence + Kadence Blocks); GeneratePress/GenerateBlocks/
Blocksy são referenciadas no registrar mas **não existem como ficheiros no build Free**
(ver §16).
## 1. `EMCP_Tools_Content_Abilities` — `includes/abilities/class-content-abilities.php`
Condição de registo: **sempre-on** (`register_groups()` linha ~158, sem guarda). Deliberadamente
agnóstico ao Elementor — opera sobre `post_content` (HTML clássico ou markup de blocos Gutenberg)
e nunca toca em `_elementor_data`.
8 tools. `check_read_permission`=`edit_posts`; `check_create_permission`=`edit_posts` (mesmo cap
que ler — mimetiza o core WP, onde publicar é gated à parte); `check_edit_permission`=`edit_posts`
+ `edit_post` por-post quando há `post_id`; `check_delete_permission`=`delete_posts` +
`delete_post` por-post.
| Tool | Input (resumo) | O que faz | Permission | RO/Destr. |
|---|---|---|---|---|
| `list-post-types` | `public_only?:bool` (default true) | Lista post types registados (nome, label, hierárquico, `supports`, taxonomias); filtra internos (`revision`, `nav_menu_item`, `wp_template`, etc.) quando `public_only`. | `check_read_permission` | RO |
| `list-taxonomies` | `post_type?`, `include_terms?:bool`, `terms_limit?:int` (≤500, default 100) | Lista taxonomias (opcionalmente filtradas por post type), com termos embutidos opcionais. | `check_read_permission` | RO |
| `create-post` | `post_type?`(default post), `title`, `content`, `excerpt`, `status?`(enum draft/publish/pending/private/future), `slug?`, `author?`, `date?`, `parent?`, `menu_order?`, `comment_status?`, `terms?:{tax:ids[]}`, `meta?:{}`, `featured_image?:{id\|url}\|null` | `wp_insert_post()`. Recusa post types internos/inexistentes; `publish` exige `publish_posts`; `author` diferente do actual exige `edit_others_posts`; meta protegida (`_`-prefix ou `is_protected_meta`) é sempre recusada salvo allowlist via filtro `emcp_tools_content_allowed_protected_meta`. `featured_image.url` faz sideload via `EMCP_Tools_Url_Guard::safe_download()` (SSRF-guarded, bloqueia hosts privados/loopback/metadata cloud e revalida cada redirect). Regista change (`EMCP_Tools_Change_Recorder::record_post_create`). | `check_create_permission` | write, não-destr. |
| `get-post` | `post_id` | Serialização completa: title/slug/status/content/excerpt/datas/parent/menu_order/comment_status/permalink/edit_link/author/terms/meta(filtrada)/featured_image/`is_elementor` (flag `_elementor_edit_mode==='builder'`). | `check_read_permission` | RO |
| `update-post` | `post_id`, campos parciais iguais ao create + `terms_mode?`(replace/append) | Update parcial via `wp_update_post()`. Captura before-image (campos + meta + terms) para rollback via `Change_Recorder::record_post_fields`. Se o slug muda num post publicado, captura o URL antigo e sugere um redirect (`with_redirect_suggestion` → `EMCP_Tools_Redirect_Abilities::push_suggestion`, só quando `EMCP_Tools_Redirect_Module::is_enabled()`). | `check_edit_permission` | write, não-destr. |
| `delete-post` | `post_id`, `force?:bool` | `force=false` (default): `wp_trash_post()`. `force=true`: snapshot completo (`Change_Recorder::snapshot_post`) antes de `wp_delete_post($id,true)` (reversível). Ambos os caminhos sugerem redirect para o URL morto. | `check_delete_permission` | write, **destrutivo** |
| `list-posts` | `post_type?`(str\|array), `status?`(str\|array), `search?`, `taxonomy?:{tax:terms[]}`(AND), `author?`, `parent?`, `per_page?`(≤100), `page?`, `orderby?`, `order?` | `WP_Query` paginado, linhas compactas (sem body de conteúdo — usar `get-post` para isso). | `check_read_permission` | RO |
| `set-post-terms` | `post_id`, `taxonomy`, `terms:(int\|string)[]`, `mode?`(replace/append/remove), `create_missing?:bool`(default true) | `wp_set_object_terms`/`wp_remove_object_terms`. Quando `create_missing=false`, resolve nomes para IDs existentes via `get_term_by`(name→slug) e descarta os que não existem (não cria). | `check_edit_permission` | write, não-destr. |
**Detalhe de implementação notável:** `apply_write_extras()` é o método partilhado usado por
`create-post`/`update-post` para aplicar `terms`/`meta`/`featured_image` — inclui o comentário
explícito no código sobre porquê carrega `wp-admin/includes/{file,media,image}.php` on-demand:
essas funções (`media_handle_sideload`) não estão carregadas em pedidos REST/WP-CLI (que é onde o
servidor MCP corre).
## 2. `EMCP_Tools_Media_Library_Abilities` — `includes/abilities/class-media-library-abilities.php`
Condição: **sempre-on** (linha ~143). Nasceu para preencher a lacuna que as tools de stock-image
não cobrem: encontrar as fotos **próprias** do cliente já na Media Library (issue #25 do repo
upstream). Recebe `EMCP_Tools_Data` no construtor (data-access layer partilhada — mesma classe
usada por várias famílias de abilities de conteúdo Elementor, não específica de media).
5 tools.
| Tool | Input (resumo) | O que faz | Permission | RO/Destr. |
|---|---|---|---|---|
| `list-media` | `search?`, `mime_type?`(default `image`; aceita "any"), `page?`, `per_page?`(≤100), `orderby?`(date/title), `order?` | `WP_Query` sobre `attachment`. O `search` do WP_Query cobre title/caption/description mas **não** alt text (vive em postmeta) — por isso resolve IDs por texto e por alt em duas queries `ids`-only e faz a união via `post__in`, sem filtros globais de query. | `check_read_permission` (`edit_posts`) | RO |
| `get-media` | `id` | Detalhe completo de 1 attachment: todos os tamanhos de imagem registados (url+dimensões via `wp_get_attachment_image_src` por tamanho), mime, filesize, alt, caption, descrição, autor, `post_parent`, metadata bruta. | `check_read_permission` | RO |
| `upload-media` | `filename`, `data`(base64, aceita prefixo `data:...;base64,`), `alt?`, `title?`, `caption?`, `description?`, `post_id?`, `convert_webp?:bool` | **Companheira de sideload-image**: sideload-image busca um URL que o SERVIDOR alcança; `upload-media` recebe bytes RAW do CLIENTE (útil quando o utilizador quer enviar um ficheiro do seu próprio computador). Decodifica base64 (limite `UPLOAD_MAX_BYTES=32MB`, filtrável via `emcp_tools_upload_media_max_bytes`), escreve para `wp_tempnam()`, valida tipo com `wp_check_filetype_and_ext()` contra `get_allowed_mime_types()` (bloqueia executáveis mesmo que um filtro afrouxe depois), entrega a `media_handle_sideload()`. Suporta `convert_webp:false` via filtro `emcp_tools_optimize_attachment` (idêntico a sideload-image). | `check_upload_permission` (`upload_files`) | write, não-destr. |
| `update-media` | `id`, `title?`, `alt?`, `caption?`, `description?` | Update parcial. `description` mapeia para `post_content` e **não** é `sanitize_text_field` (permitiria HTML legítimo — `wp_update_post` já aplica `wp_filter_post_kses` para quem não tem `unfiltered_html`). Captura before-image para rollback. | `check_edit_permission` (por-attachment) | write, não-destr. |
| `delete-media` | `id`, `confirm:true`(obrigatório), `force?:bool` | **Destrutivo e efectivamente permanente**: WordPress ignora a Trash para media a menos que `MEDIA_TRASH` esteja definido — o próprio schema avisa disto na descrição. Snapshot completo antes de apagar (post + meta + cópia trashed de todos os ficheiros) via `Change_Recorder::snapshot_attachment`, só quando não vai para trash. | `check_delete_permission` (por-attachment) | write, **destrutivo** |
## 3. `EMCP_Tools_Settings_Abilities` — `includes/abilities/class-settings-abilities.php`
Condição: **sempre-on** (linha ~224). Nome deliberadamente distinto de `EMCP_Tools_Settings_Validator`
(que valida settings de widgets Elementor — classe não relacionada).
**Decisão de design central:** NÃO expõe `get_option`/`update_option` arbitrário. Só a
**allowlist tipada e curada** em `allowlist()` — 32 chaves cobrindo os 6 ecrãs de Settings do core
(General/Reading/Writing/Discussion/Media/Permalinks). Cada entrada tem `{group, label, type
(string|int|bool|enum), writable, options?, min?, max?, pattern?}`. Notavelmente **ausentes** da
allowlist: `siteurl`/`home` (lock-out do site), `users_can_register`/`default_role` (escalada de
registo). `admin_email` está presente mas `writable=false` (só leitura).
2 tools, ambas `check_manage_permission` = `manage_options`.
| Tool | Input | O que faz | RO/Destr. |
|---|---|---|---|
| `get-settings` | `group?`(enum dos 6 ecrãs), `keys?:string[]` | Sem args devolve as 32; filtra por grupo/chaves. Cada linha inclui `value` (coagido ao tipo declarado), `writable`, e `options[]` quando é enum — **duplica como discovery** para `update-settings`. | RO |
| `update-settings` | `settings:{key:value}` | Escreve só chaves allowlisted+writable; tudo o resto (chave desconhecida, read-only, valor inválido) cai em `skipped[]` com motivo — **uma chave má nunca aborta o batch**. Coerção por tipo com clamp min/max (int), regex `pattern` (string, ex. `permalink_structure`), validação de `enum`. Se qualquer `permalink_structure`/`category_base`/`tag_base` mudar, chama `flush_rewrite_rules(false)` automaticamente e reporta `rewrite_flushed`. Regista before-map para rollback. | write, não-destr. (idempotente) |
## 4. `EMCP_Tools_Plugin_Abilities` — `includes/abilities/class-plugin-abilities.php`
Condição: **sempre-on** (linha ~229). 7 tools: 2 leitura (sempre activas) + 5 mutação
(instalação/activação/desactivação/update/delete), todas construídas sobre APIs core (`Plugin_Upgrader`,
`plugins_api`, `activate_plugin`/`deactivate_plugins`/`delete_plugins`) e guardadas por
`EMCP_Tools_Package_Guard` (classe partilhada com theme-abilities — ver comentário do ficheiro:
"as 5 tools de mutação vêm desligadas por omissão, o admin liga-as na tab Tools").
| Tool | Input | O que faz | Permission | RO/Destr. |
|---|---|---|---|---|
| `list-plugins` | `status?`(all/active/inactive) | `get_plugins()` + `get_site_transient('update_plugins')` para flag de update disponível; inclui `is_protected` (via `Package_Guard::is_protected_plugin`). | `activate_plugins` | RO |
| `search-plugins` | `search`, `per_page?`(≤50) | `plugins_api('query_plugins', …)` — pesquisa no directório wordpress.org. | `install_plugins` | RO |
| `install-plugin` | `slug`, `activate?:bool` | `plugins_api('plugin_information')` → `Plugin_Upgrader::install()`. **Fonte sempre wordpress.org — URLs arbitrários nunca são aceites** (explicitamente afirmado na descrição da tool). | `install_plugins` | write, não-destr. |
| `activate-plugin` | `plugin`(file ou slug de pasta) | `activate_plugin()`. Resolve referência via `resolve_plugin_file()` (tenta como file exacto, depois como slug de pasta). | `activate_plugins` | write, idempotente |
| `deactivate-plugin` | `plugin` | `deactivate_plugins()`. Recusa plugins protegidos (**EMCP Tools e Elementor nunca podem ser desactivados via MCP** — protege contra o agente cortar o próprio ramo em que está sentado). | `activate_plugins` | write, idempotente |
| `update-plugin` | `plugin` | `wp_update_plugins()` refresca transient; se não há update pendente devolve `up_to_date:true` sem chamar o upgrader. Recusa plugins protegidos (mesma lista de deactivate). | `update_plugins` | write, não-destr. |
| `delete-plugin` | `plugin` | `delete_plugins()`. Recusa protegidos E recusa qualquer plugin **activo** (obriga a desactivar primeiro). | `delete_plugins` | write, **destrutivo** |
## 5. `EMCP_Tools_Theme_Abilities` — `includes/abilities/class-theme-abilities.php`
Condição: **sempre-on** (linha ~233). Espelho exacto do padrão de plugin-abilities mas para temas
(`Theme_Upgrader`, `themes_api`, `switch_theme`/`delete_theme`), mesmas 5+1 = 6 tools (sem
`search`+`install`+`switch`+`update`+`delete`+`list` = 6, plugins tem 7 porque tem
activate+deactivate separados; temas só têm `switch-theme`).
| Tool | Input | O que faz | Permission | RO/Destr. |
|---|---|---|---|---|
| `list-themes` | (nenhum) | `wp_get_themes()` + `get_site_transient('update_themes')`; inclui `parent` (para child themes) e `is_active`. | `switch_themes` | RO |
| `search-themes` | `search`, `per_page?`(≤50) | `themes_api('query_themes', …)` sobre wordpress.org. | `install_themes` | RO |
| `install-theme` | `slug`, `activate?:bool` | `themes_api('theme_information')` → `Theme_Upgrader::install()`. Fonte sempre wordpress.org. | `install_themes` | write, não-destr. |
| `switch-theme` | `stylesheet` | `switch_theme()`. Recusa temas com `$theme->errors()` (load errors). | `switch_themes` | write, idempotente |
| `update-theme` | `stylesheet` | `wp_update_themes()` refresca transient; `up_to_date:true` sem upgrade se nada pendente. | `update_themes` | write, não-destr. |
| `delete-theme` | `stylesheet` | `delete_theme()`. Recusa o tema activo **e o parent do tema activo** (`Package_Guard::active_theme_stylesheets()`). | `delete_themes` | write, **destrutivo** |
## 6. `EMCP_Tools_User_Abilities` — `includes/abilities/class-user-abilities.php`
Condição: **sempre-on** (linha ~238). O comentário de cabeçalho do ficheiro resume a filosofia:
"the security boundary is the design" — **sem tool de delete**, **sem tool de mudança de role**,
password sempre auto-gerada e nunca devolvida.
4 tools. Guarda de privilégio central: `protected_caps()` = `{manage_options, promote_users,
delete_users, edit_users, manage_network}`. `user_has_admin_caps($id)` verifica se um utilizador
tem QUALQUER uma destas caps (via `user_can`) — se sim, é **intocável** por `update-user`.
`role_has_admin_caps($role)` faz o mesmo a nível de role, para bloquear `create-user` de atribuir
uma role admin-grade.
| Tool | Input | O que faz | Permission | RO/Destr. |
|---|---|---|---|---|
| `list-users` | `role?`, `search?`, `per_page?`(≤100), `page?`, `orderby?`(registered/display_name/ID), `order?` | `WP_User_Query`. Linhas compactas (id, username, display_name, email, roles, registered, post_count) — nunca password/auth. | `list_users` | RO |
| `get-user` | `id` | Detalhe: username/email/display_name/first_name/last_name/nickname/url/description/roles/registered/post_count + flag `is_admin` (= `user_has_admin_caps`, sinaliza que `update-user` vai recusar). | `list_users` | RO |
| `create-user` | `username`, `email`, `role?`(default `subscriber`), `first_name?`, `last_name?`, `display_name?`, `url?`, `description?` | Password gerada com `wp_generate_password(24, true, true)` (nunca devolvida). Envia email de "definir password" via `wp_send_new_user_notifications($id, 'user')`. Recusa role inexistente ou admin-grade. **Anti-enumeração:** erros `existing_user_login`/`existing_user_email` são normalizados para uma mensagem genérica ("that username or email is not available") para que a tool não sirva para enumerar contas existentes. | `create_users` | write, não-destr. |
| `update-user` | `id`, `email?`, `first_name?`, `last_name?`, `display_name?`, `nickname?`, `url?`, `description?` | Recusa se o alvo tem admin-caps. Constrói o update **só** a partir dos campos permitidos — `role`/`password` **nunca são sequer lidos** do input, logo nunca podem mudar aqui (não é uma verificação em runtime, é ausência estrutural do campo). Captura before-image para rollback. | `edit_users` | write, não-destr. |
## 7. `EMCP_Tools_Nav_Menu_Abilities` — `includes/abilities/class-nav-menu-abilities.php`
Condição: **sempre-on** (linha ~243). Padrão de **dois dispatchers** (`menu-read`/`menu-write`),
o mesmo padrão que as integrações de tema (§9-13) e as abilities ACF/SEO/forms (doc 08) usam:
cada tool aceita `{operation, arguments}`; chamar sem `operation` devolve o catálogo de operações
disponíveis (nome + descrição) — auto-descoberta sem precisar de schemas JSON per-operation
separados. Ambos os dispatchers usam a mesma permissão: `edit_theme_options`.
**13 operações** no total:
| Operação | Modo | Argumentos | O que faz |
|---|---|---|---|
| `list-menus` | read | `{}` | Todos os menus: id, name, slug, item count, locations atribuídas. |
| `get-menu` | read | `{menu: id\|slug\|name}` | Um menu + a sua árvore de items aninhada (`build_item_tree`, construída a partir da lista flat via `menu_item_parent`). |
| `list-locations` | read | `{}` | Locations de menu registadas pelo tema + qual menu (se algum) está atribuído a cada uma. |
| `render` | read | `{menu\|location, depth?, container?, container_class?, menu_class?, menu_id?}` | `wp_nav_menu(['echo'=>false])` → devolve HTML. Útil para embutir num header custom. |
| `create-menu` | write | `{name}` | `wp_create_nav_menu()`. |
| `rename-menu` | write | `{menu, name}` | `wp_update_nav_menu_object()`. |
| `delete-menu` | write | `{menu}` | `wp_delete_nav_menu()`. |
| `assign-location` | write | `{menu, location}` | `set_theme_mod('nav_menu_locations', …)`. Valida que a location está registada pelo tema. |
| `unassign-location` | write | `{location}` | Remove a atribuição de uma location. |
| `add-item` | write | `{menu, type, object_id?, object?, url?, title?, parent?, position?, target?, classes?, description?, xfn?}` | `wp_update_nav_menu_item()`. `resolve_item_type()` valida `type` (custom/page/post/CPT/category/taxonomy) contra objectos reais existentes — nunca cria um item apontando para um post/termo inexistente. Items custom exigem `url`+`title`. |
| `update-item` | write | `{item, title?, url?, parent?, position?, target?, classes?, description?, xfn?}` | Update parcial que **preserva** todos os campos não especificados (`merge_existing_item()` lê o item actual via `wp_setup_nav_menu_item()` antes de mesclar as mudanças — evita a armadilha comum de `wp_update_nav_menu_item` de apagar campos omitidos). |
| `delete-item` | write | `{item}` | `wp_delete_post($id, true)` (items de menu são posts `nav_menu_item`). |
| `reorder-items` | write | `{menu, items: [{id, parent?, position}]}` | Reordena/reparenta múltiplos items numa chamada, preservando os restantes campos de cada um (reutiliza `merge_existing_item`). Ignora silenciosamente items inválidos/de outro menu/com parent inválido, e conta só os efectivamente actualizados. |
**Validações estruturais notáveis:** `validate_parent()` garante que um parent proposto é um item
de menu real e pertence **ao mesmo menu** (não permite cruzar menus). `resolve_item_type()` faz a
mesma validação de existência para `add-item` que `set-post-terms` faz para termos — nunca cria
referências soltas.
## 8. `EMCP_Tools_Image_Resize_Abilities` — `includes/abilities/class-image-resize-abilities.php`
Condição: **condicional dupla** (registrar linha ~149) — só regista quando `class_exists(
'EMCP_Tools_Image_Resize_Abilities' )` **E** `class_exists( 'EMCP_Tools_Image_Optimization_Module'
)` **E** `EMCP_Tools_Image_Optimization_Module::module_is_active()`. Ou seja: depende do módulo
"Image Optimization" (um dos 9 módulos opcionais documentados no doc 10) estar activo — reutiliza
a maquinaria de backup+compressão+WebP desse módulo (`EMCP_Tools_Image_Resizer::resize()`, classe
do módulo, não desta ability).
1 tool: `resize-media`. `attachment_id`, `width?`, `height?` (pelo menos um dos dois; escala
mantendo aspect ratio), `crop?:bool` (hard-crop exacto width×height, requer ambos). Permissão:
`edit_post($attachment_id)` se especificado, senão `upload_files`. O **ID do attachment e as suas
URLs mantêm-se os mesmos** (resize in-place), o original é feito backup (reversível), e todos os
sub-tamanhos + WebP são regenerados. Se o cap de dimensão máxima do módulo for menor que o alvo
pedido, o cap aplica-se silenciosamente.
## 9. `EMCP_Tools_Theme_Integration` (abstract) — `includes/abilities/class-theme-integration.php`
**Serviço de suporte**, não regista abilities por si — é a **classe base abstracta** para todas as
integrações de tema/framework (§10-13 + os Pro-only do §16). Implementa o padrão "dois
dispatchers" de forma genérica e reutilizável.
Contrato abstracto que cada subclasse implementa: `id()` (usado para construir os nomes das tools
`emcp-tools/<id>-read`/`-write`), `label()`, `is_available()` (gate de disponibilidade — tema
activo ou plugin activo), `operations()` (o mapa `name => {mode, run, perm, desc}`).
A base fornece:
- **`register()`** — regista os dois dispatchers via `emcp_tools_register_ability()`. Ambos usam
o mesmo `dispatch_schema()` genérico (`{operation?, arguments?}`).
- **`can_read()`/`can_write()`** — gate grosseiro **da tool** (`edit_theme_options` por omissão);
cada subclasse pode sobrepor (Spectra e Kadence Blocks sobrepõem para `edit_posts`, porque
constroem conteúdo, não opções de tema — ver §12/§13).
- **`dispatch()`** — resolve a operação pelo nome; se `operation` vazio devolve o **catálogo de
descoberta**; valida que o modo (read/write) bate; corre a **permissão específica da operação**
(`$op['perm']`) — nunca confia só no gate grosseiro do tool, cada operação individual
re-verifica. Um erro 404 `unknown_operation` ou 403 `forbidden` tem `status` no `WP_Error` data.
- **`capabilities()`** — descritor normalizado (`supports_patterns`, `supports_preview`,
`styles_model`: none/uniqueid/styles-object/attributes) que um agente lê para saber, sem
hard-coding por-framework, se este pack tem uma rota de inserção "editor-válida" (patterns) ou
preview, e como o pack se auto-estiliza. Default `{false, false, 'none'}`; cada subclasse
sobrepõe.
**Blueprint chave:** este é o padrão a copiar quase 1:1 numa réplica própria — uma classe base
abstracta de ~250 linhas que qualquer integração de tema/plugin de terceiros herda, ganhando
discovery + dispatch + permission-per-operation de graça. Adicionar um framework novo (ex.
GeneratePress) é só escrever a subclasse concreta com `operations()`.
## 10. `EMCP_Tools_Active_Theme_Integration` — `includes/abilities/class-active-theme-integration.php`
`id()`='theme'. `is_available()` = **sempre true** (funciona com qualquer tema activo — é o pack
agnóstico-de-framework). Descrição do ficheiro: "Building pages reuses the Gutenberg/Elementor
tools; this integration supplies the context the agent reasons over."
4 operações:
| Operação | Modo | Argumentos | O que faz |
|---|---|---|---|
| `get-theme-context` | read | `{}` | Identidade + capacidades do tema activo: stylesheet/parent, `is_child`, framework detectado (contra `KNOWN_FRAMEWORKS = {astra, kadence, generatepress, oceanwp, blocksy, neve, hello-elementor}`), `is_block_theme` (`wp_is_block_theme()`), nome/versão, `template_dir`, `theme supports` probed (`PROBED_SUPPORTS`: post-thumbnails, custom-logo, editor-styles, wp-block-styles, align-wide, responsive-embeds, custom-background, html5), menu locations registadas, `has_child` (`Child_Theme_Builder::child_exists()`). **"Call this first"** — é o ponto de entrada de contexto antes de qualquer outra operação de tema. |
| `get-mods` | read | `{}` | Todos os `theme_mods` do tema activo (estado do customizer). |
| `set-mods` | write | `{values: {key: value}}` | `set_theme_mod()` por chave, salvo `REFUSED_MODS = {nav_menu_locations, sidebars_widgets, custom_css_post_id}` — mods estruturais recusados (esses domínios têm as suas próprias tools: nav-menu, widgets não têm tool própria, custom CSS é módulo separado). |
| `create-child-theme` | write, `perm`=`can_manage_theme` (`switch_themes`+`edit_theme_options`) | `{confirm:true}`(obrigatório) | Delega para `EMCP_Tools_Child_Theme_Builder::create()` (§14). Exige `confirm:true` porque muda o tema activo E liga escrita de ficheiros. |
## 11. `EMCP_Tools_Astra_Integration` — `includes/abilities/class-astra-integration.php`
`id()`='astra'. `is_available()` = `'astra' === get_template()`. Lê/escreve a única option
`astra-settings` sobre uma **allowlist curada de 17 chaves** (`ALLOWLIST` const), agrupadas em
`colors`/`typography`/`layout`/`header-footer` — a mesma forma genérica get/update que
`EMCP_Tools_Settings_Abilities` usa para o core WordPress (§3), aplicada ao option próprio do
Astra. Allowlist explicitamente marcada como "verified against Astra 4.13.4 on the dev site".
2 operações: `get-settings` (`{group?, keys?}`) e `update-settings` (`{values:{key:value}}`,
chaves não-allowlisted em `skipped[]`). `read_value()` prefere o resolver nativo `astra_get_option()`
quando disponível (respeita o default do tema quando a option está ausente); senão cai no valor
bruto da option. **Detalhe de invalidação de cache notável:** Astra só refresca o seu CSS dinâmico
em cache no hook `customize_save_after`, **não** numa escrita simples de option — por isso
`execute_update_settings()` chama explicitamente `astra_clear_all_assets_cache()` após qualquer
update, senão as mudanças ficariam invisíveis até o próximo save do Customizer.
## 12. `EMCP_Tools_Spectra_Integration` — `includes/abilities/class-spectra-integration.php`
`id()`='spectra'. `is_available()` = `EMCP_Tools_Spectra_Catalog::is_active()`. Sobrepõe
`can_read()`/`can_write()` para `edit_posts` (constrói **conteúdo**, não opções de tema).
`capabilities()`: `{supports_patterns:false, supports_preview:false, styles_model:'attributes'}`.
3 operações — o padrão discover→inspect→act espelhando o catálogo de widgets Elementor:
| Operação | Modo | Argumentos | O que faz |
|---|---|---|---|
| `list-blocks` | read | `{category?, search?}` | Catálogo compacto (via `Spectra_Catalog::blocks_index()`). |
| `get-block-schema` | read | `{name?\|names?[], full?}` | Atributos reais + defaults (`Spectra_Catalog::real_attributes()`) + markup de exemplo gerado. Reporta também `shared_attributes` (ver §12.1) e um `note` quando trunca à `DEFAULT_CAP=30`. |
| `add-block` | write | `{post_id, block, attributes?, position?}` | Constrói o array de bloco parseado (`build_block()`), insere-o via `EMCP_Tools_Block_Tree::insert()` (serviço partilhado com Gutenberg — doc 02) na árvore existente do post, serializa de volta com `serialize_block()`, grava com `wp_update_post`. Gera `block_id` via `EMCP_Tools_Id_Generator::generate()`. |
### 12.1 `EMCP_Tools_Spectra_Catalog` — `includes/blocks-catalog/class-spectra-catalog.php`
Serviço de suporte (não regista abilities). Nada é adivinhado — as duas metades vêm do próprio
Spectra:
- **Lista de blocos** — `UAGB_Block_Module::get_blocks_info()` (a própria API pública do Spectra).
- **Atributos por bloco** — lidos directamente do ficheiro fonte do Spectra
`includes/blocks/<slug>/attributes.php` (via `require`, não introspecção de registry) — porque
Spectra não regista todos os atributos no `WP_Block_Type_Registry` core.
`STRUCTURE` const documenta hints estruturais que **não** podem vir de `attributes.php`: quais
blocos precisam de innerBlocks template (`uagb/buttons`→`uagb/buttons-child`, etc., 8 pares
documentados) e quais são dinâmicos/server-rendered (`post-grid`, `post-carousel`, `google-map`,
`forms`, etc. — `add-block` emite markup self-closing para estes).
**`SHARED_ATTRS`** é o achado mais interessante desta classe: documenta atributos nativos e reais
do `uagb/container` que são registados via um helper partilhado (`UAGB_Block_Helper`) em vez de
chaves literais em `attributes.php` — por isso **não aparecem** em `real_attributes()`, mas
existem e são editáveis (background image/video com overlay, border-radius por canto, box-shadow).
O comentário no código é explícito: "so they DO NOT appear in real_attributes()/get-block-schema,
yet they are real, editable attributes an agent should use instead of a core/html workaround" —
inclui até a gotcha exacta de que o overlay em gradiente vem de `gradientValue`, **não** de
`gradientOverlayColor1/2` como seria intuitivo.
## 13. `EMCP_Tools_Kadence_Integration` (theme settings) — `includes/abilities/class-kadence-integration.php`
`id()`='kadence'. `is_available()` = `'kadence' === get_template()`. Lê/escreve `theme_mods` do
Kadence (não uma option única como Astra) sobre uma **allowlist de 16 chaves** agrupadas em
`palette`/`colors`/`typography`/`layout`/`buttons`/`header-footer`. Cada entrada da allowlist
carrega um `shape` (string descritiva da forma do objecto esperado) — porque, ao contrário de
Astra, os valores de Kadence são **objectos estruturados**, não escalares (ex.
`link_color = { highlight: "palette1", "highlight-alt": "palette2", style: "standard" }`), e
referenciam uma **paleta global de 9 slots** (`palette1`..`palette9`) definida em `global_palette`.
2 operações: `get-settings`/`update-settings`, mesma forma que Astra. `read_value()` prefere
`\Kadence\kadence()->option($key)` (resolver nativo com theme_mod+default embutido). **Sem
invalidação de cache explícita** — comentário do ficheiro nota que "Kadence renders dynamic CSS
inline per request, so there is no cache to invalidate" (contraste directo com o gotcha do Astra
em §11).
## 14. `EMCP_Tools_Kadence_Blocks_Integration` — `includes/abilities/class-kadence-blocks-integration.php`
`id()`='kadence-blocks'. `is_available()` = `EMCP_Tools_Kadence_Blocks_Catalog::is_active()`.
Independente do tema activo (tal como Spectra — Kadence Blocks é um plugin, não amarrado ao tema
Kadence). `can_read()`/`can_write()` = `edit_posts`. `capabilities()`:
`{supports_patterns:true, supports_preview:true, styles_model:'uniqueid'}`.
**5 operações** — a mais rica das integrações de tema, porque acrescenta acesso à Kadence
Prebuilt Library (patterns prontos) por cima do padrão discover→inspect→act:
| Operação | Modo | Argumentos | O que faz |
|---|---|---|---|
| `list-blocks` | read | `{category?, search?}` | Catálogo curado dos 32 blocos top-level (via `Kadence_Blocks_Catalog`). |
| `get-block-schema` | read | `{name?\|names?[], full?}` | Atributos reais do `WP_Block_Type_Registry` (Kadence, ao contrário do Spectra, regista schemas completos com defaults no core registry — não precisa de ler ficheiro fonte). Inclui `inner_blocks` hint para containers, `content_attributes` para campos RichText/HTML-source, e um `editor_note` fixo (ver abaixo). |
| `add-block` | write | `{post_id, block, attributes?, position?}` | Gera `uniqueID` (chave CSS por-bloco do Kadence), escafolda inner blocks de container conforme `STRUCTURE`, **renderiza campos RichText directamente no HTML guardado** (não no JSON de atributos — ver `render_source_fields()` abaixo). |
| `list-patterns` | read | `{category?, search?, include_pro?, categories?:bool}` | Lista patterns da Kadence Design Library (cache local em `uploads/kadence_blocks_library/`). `categories:true` devolve só os nomes de categoria. |
| `insert-pattern` | write | `{post_id, pattern, position?, localize_images?:bool}` | Insere markup **canónico** de um pattern (busca via `Kadence_Blocks_Prebuilt_Library_REST_Controller::get_pattern_content` — o próprio controller REST do Kadence, chamado internamente sem HTTP). `localize_images:true` transfere as imagens do pattern para a Media Library via o próprio `process_pattern` do Kadence (requer `upload_files`). |
**Nota de robustez notável em `add-block`:** o output devolve sempre um `note` explícito — "Kadence
blocks use a static JS save(), so a headlessly-inserted block renders correctly on the front end
but the block editor may show 'Attempt recovery' on this block — one click regenerates valid
markup and preserves the content." Isto é honestidade de design: blocos Kadence construídos à mão
(não via pattern) não batem byte-a-byte com o `save()` estático do JS do bloco, então o editor
Gutenberg vai reclamar (recuperável, sem perda de dados) — só o caminho `insert-pattern` (markup
canónico do próprio Kadence) evita esse aviso.
**`render_source_fields()`** é o mecanismo mais complexo desta classe: para cada atributo que o
registry marca como `source: html|rich-text` (ex. o `content` do Advanced Heading), renderiza o
valor directamente em HTML dentro do bloco serializado usando o **selector real** registado por
esse atributo (tag+classe), em vez de o guardar no JSON de `attrs` — porque é assim que WordPress
lê esses valores de volta (parseados do HTML, não do JSON). Trata o caso especial "Advanced
Heading" (`selector_element()`): a sua tag vem de `htmlTag` (h1-h6/p/div/span), e a classe CSS
correcta é `kt-adv-heading{uniqueID}` — não o que o `selector` bruto do registry sugeriria.
### 14.1 `EMCP_Tools_Kadence_Blocks_Catalog` — `includes/blocks-catalog/class-kadence-blocks-catalog.php`
Serviço de suporte. Diferente do Spectra: ambas as metades (lista + atributos) vêm do
`WP_Block_Type_Registry` **core** — Kadence regista schemas de atributos completos com defaults,
não precisa de ler ficheiro fonte. `TOP_LEVEL` const lista os 32 slugs "placeable" (exclui blocos
filho/estruturais como `column-child`, `singlebtn`, `listitem`, `table-row`, que são inseridos via
`STRUCTURE`). `TITLES`/`CATEGORY`/`HIGHLIGHT`/`STRUCTURE` são curadoria manual (explicitamente
"live-verified on Kadence Blocks 3.7.9" contra uma spec própria referenciada no comentário).
`STRUCTURE['kadence/rowlayout']` tem uma gotcha documentada no comentário: `colLayout` **tem** de
ser não-vazio ("equal") ou o editor mostra o picker "Select Your Layout" do Kadence em vez das
colunas — "the frontend renders regardless" mas a experiência de edição fica quebrada sem isto.
### 14.2 `EMCP_Tools_Kadence_Pattern_Library` — `includes/blocks-catalog/class-kadence-pattern-library.php`
Serviço de suporte. Lê o **cache local de metadados** (`uploads/kadence_blocks_library/*.json`,
distingue o ficheiro de metadata do de preview-HTML pela presença de `slug`+`name` nas entradas) e
delega a obtenção do markup real ao **controller REST do próprio Kadence**
(`Kadence_Blocks_Prebuilt_Library_REST_Controller`), invocado **internamente** (constrói um
`WP_REST_Request` e chama o método directamente — sem round-trip HTTP). Nota explícita no cabeçalho
do ficheiro: se a cache local estiver vazia, o utilizador precisa de abrir a Kadence Design Library
uma vez no editor de blocos para a popular — a tool `list-patterns` devolve esse aviso quando
detecta catálogo vazio.
## 15. `EMCP_Tools_Child_Theme_Builder` — `includes/class-child-theme-builder.php`
Serviço de suporte, usado por `Active_Theme_Integration::execute_create_child_theme` (§10).
**Deliberadamente conservador**, três garantias explícitas no comentário de cabeçalho:
1. Só cria um child do parent **actualmente activo** — nunca toca noutro tema.
2. Recusa quando o tema activo **já é um child** (`is_active_a_child()`) — nunca cria um
"neto" (grandchild).
3. **Idempotente** — um child existente é apenas activado (`switch_theme`), nunca sobrescrito.
`create()`: cria a pasta (`wp_mkdir_p`), gera `style.css` mínimo (cabeçalho `Theme Name`/`Template`/
`Version`/`Description`) e `functions.php` mínimo (enfileira o `style.css` do parent via
`wp_enqueue_scripts`), depois `switch_theme($slug)`. Escreve via `$wp_filesystem->put_contents()`
quando inicializado, senão `file_put_contents()` directo — o mesmo padrão dual usado pelas
Filesystem tools (doc 07). Devolve `{child, parent, directory, activated, created}`.
**É o enabler estrutural para edição de ficheiros de tema pelo agente**: depois de criar o child,
o agente edita `style.css`/`functions.php`/templates via as tools de Filesystem (ABSPATH-confined,
gated por `edit_files`, com backup e audit-log — doc 07), nunca directamente aqui.
## 16. Pro-only confirmados ausentes: GeneratePress, GenerateBlocks, Blocksy
`class-ability-registrar.php` linhas 342-355 referencia 4 classes adicionais de integração de
tema, seguindo o comentário explícito no código: *"GeneratePress + GenerateBlocks (Pro; classes
only present when Pro loaded)"* e *"Blocksy (Pro): blocks + Companion extensions."*
```php
if ( class_exists( 'EMCP_Tools_GeneratePress_Integration' ) ) { … }
if ( class_exists( 'EMCP_Tools_GenerateBlocks_Integration' ) ) { … }
if ( class_exists( 'EMCP_Tools_Blocksy_Blocks_Integration' ) ) { … }
if ( class_exists( 'EMCP_Tools_Blocksy_Extensions_Integration' ) ) { … }
```
**Confirmado por listagem directa do directório** `includes/abilities/` neste build Free (19-08-2026,
48 ficheiros `class-*.php` listados): **nenhum ficheiro** `class-generatepress-integration.php`,
`class-generateblocks-integration.php`, `class-blocksy-blocks-integration.php` ou
`class-blocksy-extensions-integration.php` existe. As 4 classes são referenciadas apenas pelo
`class_exists()` gate — em produção Free, este gate é sempre `false`; as 4 integrações só passam a
existir (ficheiro + classe) quando o build **Pro** (`emcp-pro/`) está instalado e activo.
Cada uma seguiria, por analogia estrutural às 5 integrações Free já lidas (§10-14), o mesmo padrão
herdado de `EMCP_Tools_Theme_Integration` (§9):
| Classe (Pro-only, código não acessível) | Gating no registrar | Padrão inferido por analogia (não confirmado) |
|---|---|---|
| `EMCP_Tools_GeneratePress_Integration` | `class_exists()` | `id()`='generatepress' provavelmente; `is_available()` provavelmente `'generatepress' === get_template()` — tema settings via GP Premium/GenerateBlocks options, seguindo o molde Astra/Kadence (§11/§13). |
| `EMCP_Tools_GenerateBlocks_Integration` | `class_exists()` | Pack de blocos GenerateBlocks (catálogo + add-block), seguindo o molde Spectra/Kadence Blocks (§12/§14) — seria o "blocks/builder" companion do GeneratePress. |
| `EMCP_Tools_Blocksy_Blocks_Integration` | `class_exists()` | Pack de blocos Blocksy (a Blocksy tem os seus próprios blocos Gutenberg nativos). |
| `EMCP_Tools_Blocksy_Extensions_Integration` | `class_exists()` | Gestão de "Extensions" do Blocksy (o painel de extensões modulares do tema — ex. Content Blocks, Custom Fonts, Ecommerce, etc.), provavelmente um dispatcher settings-like análogo ao Astra. |
**Não inventar schemas nem comportamento além desta inferência estrutural** — sem acesso ao código
fonte destas 4 classes, este é o limite do que se pode documentar com honestidade.
## Blueprint para réplica
**Copiar quase 1:1:**
- **`EMCP_Tools_Theme_Integration` (abstract, §9)** — o padrão de 2 dispatchers + discovery +
permission-per-operation é genérico, pequeno (~250 linhas), e escala bem para qualquer novo
framework/plugin de terceiros. É o único ponto de extensão que vale a pena manter exactamente
como está numa réplica.
- **`EMCP_Tools_Settings_Abilities` (§3) e o padrão allowlist tipada** — a decisão de nunca expor
`get_option`/`update_option` cru é a protecção certa; a allowlist com `{type, writable, min,
max, pattern, options}` é suficientemente expressiva sem ser um mini-JSON-Schema paralelo.
Reutilizar esta forma para settings de qualquer plugin de terceiros (Astra/Kadence já o fazem).
- **Guarda de privilégio de `User_Abilities` (§6)** — `protected_caps()` + `user_has_admin_caps`/
`role_has_admin_caps` é uma barreira de segurança simples e correcta (nenhuma tool de delete,
role/password nunca lidos do input de update). Copiar tal e qual.
- **`Package_Guard` (referenciado por Plugin/Theme abilities, §4/§5)** — lista de protecção
(nunca desactivar/apagar/actualizar o próprio plugin nem o Elementor) é um padrão de
auto-preservação que qualquer plugin MCP-serving precisa de replicar (senão o agente pode
cortar o próprio ramo em que está sentado).
- **`Child_Theme_Builder` (§15)** — as 3 garantias conservadoras (só o parent activo, nunca um
grandchild, idempotente) são exactamente as invariantes certas para este tipo de operação.
**Simplificar:**
- **Kadence Blocks `render_source_fields()`/`selector_element()` (§14)** — é código correcto mas
bastante intrincado (resolve selectors CSS de volta para tag+classe HTML por heurística). Numa
réplica que não precise de suportar Kadence Blocks especificamente, este é o tipo de
complexidade a NÃO reinventar — só vale a pena se formos mesmo integrar Kadence Blocks.
- **`Spectra_Catalog::real_attributes()` a fazer `require` de um ficheiro fonte de outro plugin**
(§12.1) é frágil a mudanças de versão do Spectra (o caminho do ficheiro é hard-coded); preferir,
quando possível, ler do `WP_Block_Type_Registry` (como Kadence faz) em vez de abrir ficheiros
fonte de terceiros directamente — só recorrer a isto quando o plugin de terceiros não regista
todos os atributos no registry core (razão pela qual o Spectra precisou desta técnica).
**Deixar de fora (a menos que haja procura real de clientes):**
- As 4 integrações Pro-only (§16) — só valem a pena reimplementar quando houver um cliente
concreto a usar GeneratePress/GenerateBlocks/Blocksy. Sem acesso ao código nem à base de
utilizadores, seria trabalho especulativo.
- `Plugin_Abilities`/`Theme_Abilities` install/update/delete completos (§4/§5) — install/delete
arbitrário de plugins/temas via MCP é uma superfície de risco elevada (mesmo restrita a
wordpress.org); para um clone interno de uso próprio da Descomplicar, ponderar reduzir a
read-only (`list`/`search`) + `activate`/`deactivate`, deixando install/update/delete fora do
MCP por preferir esse fluxo pelo wp-admin/WP-CLI directamente.
**Riscos/gotchas não óbvios encontrados no código (citações):**
- Astra: cache de CSS dinâmico só invalida em `customize_save_after`, **não** numa escrita de
option simples — sem a chamada explícita a `astra_clear_all_assets_cache()`, updates via MCP
ficariam invisíveis até o próximo save do Customizer (§11).
- Spectra: overlay de imagem de fundo em gradiente vem de `gradientValue`, **não** de
`gradientOverlayColor1/2` como seria intuitivo — documentado explicitamente no
`SHARED_ATTRS` do próprio código (§12.1).
- Kadence Blocks `rowlayout`: `colLayout` vazio faz o editor mostrar um picker de layout em vez
das colunas — "the frontend renders regardless" mas a experiência de edição fica quebrada; fix
é forçar `colLayout: 'equal'` como default quando o caller não o especifica (§14.1).
- Kadence Blocks construídos à mão (não via pattern) disparam "Attempt recovery" no editor porque
não batem byte-a-byte com o `save()` estático JS — o front-end renderiza correctamente na mesma;
só `insert-pattern` (markup canónico do próprio Kadence) evita o aviso (§14).
- `upload-media`/`create-post` (featured_image sideload): as funções de sideload
(`media_handle_sideload` e dependências) vivem em `wp-admin/includes/*` que **não** está
carregado em pedidos REST/WP-CLI — têm de ser `require_once`'d on-demand a cada chamada (§1, §2).
- `create-user`: erros de conta existente são normalizados para uma mensagem genérica
precisamente para impedir que a tool sirva de oráculo de enumeração de contas (§6).
## Fonte
Leitura directa (19-08-2026) de: `includes/abilities/class-content-abilities.php`,
`includes/abilities/class-media-library-abilities.php`,
`includes/abilities/class-settings-abilities.php`,
`includes/abilities/class-plugin-abilities.php`,
`includes/abilities/class-theme-abilities.php`,
`includes/abilities/class-user-abilities.php`,
`includes/abilities/class-nav-menu-abilities.php`,
`includes/abilities/class-image-resize-abilities.php`,
`includes/abilities/class-theme-integration.php`,
`includes/abilities/class-active-theme-integration.php`,
`includes/abilities/class-astra-integration.php`,
`includes/abilities/class-spectra-integration.php`,
`includes/abilities/class-kadence-integration.php`,
`includes/abilities/class-kadence-blocks-integration.php`,
`includes/blocks-catalog/class-kadence-blocks-catalog.php`,
`includes/blocks-catalog/class-kadence-pattern-library.php`,
`includes/blocks-catalog/class-spectra-catalog.php`,
`includes/class-child-theme-builder.php`; grep de
`includes/abilities/class-ability-registrar.php` (linhas 135-165, 220-250, 320-380) para as
condições de registo exactas e a confirmação da lista de 4 classes Pro-only referenciadas mas não
implementadas; listagem directa de `includes/abilities/` (48 ficheiros) para confirmar a ausência
física dos ficheiros GeneratePress/GenerateBlocks/Blocksy neste build Free. Cruzado com
`docs/00-ARQUITECTURA.md` (mesma sessão) para o contrato `emcp_tools_register_ability()`.