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).
495 lines
44 KiB
Markdown
495 lines
44 KiB
Markdown
# 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()`.
|