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).
44 KiB
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:
- WordPress core puro (content, media, settings, plugins, themes, users, nav menus) — sempre activas, sem dependência de Elementor nem de nenhum framework de terceiros.
- 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_postpor-post quando hápost_id;check_delete_permission=delete_posts+delete_postpor-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 viaemcp_tools_register_ability(). Ambos usam o mesmodispatch_schema()genérico ({operation?, arguments?}).can_read()/can_write()— gate grosseiro da tool (edit_theme_optionspor omissão); cada subclasse pode sobrepor (Spectra e Kadence Blocks sobrepõem paraedit_posts, porque constroem conteúdo, não opções de tema — ver §12/§13).dispatch()— resolve a operação pelo nome; seoperationvazio 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 404unknown_operationou 403forbiddentemstatusnoWP_Errordata.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(viarequire, não introspecção de registry) — porque Spectra não regista todos os atributos noWP_Block_Type_Registrycore.
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:
- Só cria um child do parent actualmente activo — nunca toca noutro tema.
- Recusa quando o tema activo já é um child (
is_active_a_child()) — nunca cria um "neto" (grandchild). - 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."
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 exporget_option/update_optioncru é 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 fazerrequirede 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 doWP_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_Abilitiesinstall/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 aastra_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 degradientOverlayColor1/2como seria intuitivo — documentado explicitamente noSHARED_ATTRSdo próprio código (§12.1). - Kadence Blocks
rowlayout:colLayoutvazio 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çarcolLayout: '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_sideloade dependências) vivem emwp-admin/includes/*que não está carregado em pedidos REST/WP-CLI — têm de serrequire_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().