# 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 (`-read`/`-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/-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//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()`.