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).
458 lines
37 KiB
Markdown
458 lines
37 KiB
Markdown
# 08 — Integrações com plugins de terceiros (ACF, Meta Box, Forms, SEO, WooCommerce)
|
|
|
|
Fonte: leitura directa do código-fonte `emcp-tools` v3.12.1 (build Free), instalado em
|
|
`emanuelalmeida.pt` (`/home/ealmeida/emanuelalmeida.pt/wp-content/plugins/emcp-tools/`),
|
|
19-08-2026. Cobre as classes que ligam o EMCP Tools a dados/estruturas geridas por outros
|
|
plugins: ACF, Meta Box, plugins de formulários (Contact Form 7 no Free) e plugins de SEO
|
|
(Slim SEO no Free), mais o inventário de integrações Pro-only que a árvore Free referencia
|
|
mas não contém (WooCommerce, 8 plugins de formulários adicionais, 6 plugins de SEO
|
|
adicionais, 2 pacotes de widgets Elementor, e a Ultimate Addons for Elementor).
|
|
|
|
Contexto arquitectural (ver `docs/00-ARQUITECTURA.md`): toda ability regista-se via
|
|
`emcp_tools_register_ability()` — nunca `wp_register_ability()` directamente —, que normaliza o
|
|
schema, envolve o `execute_callback` num veto de escrita (`emcp_tools_before_write`) e num
|
|
`normalize_result()` (garante retorno sempre objecto JSON), antes de delegar ao core. O gating
|
|
de cada grupo vem de `includes/abilities/class-ability-registrar.php` (lido na íntegra nesta
|
|
tarefa) — os excertos exactos são reproduzidos abaixo, não parafraseados.
|
|
|
|
## 0. Padrão comum às cinco integrações deste documento: o "dispatcher de dois braços"
|
|
|
|
Todas as cinco famílias (ACF, Meta Box, Forms, SEO — WooCommerce presumivelmente também,
|
|
mas Pro) seguem a MESMA forma: em vez de registar N abilities MCP individuais (uma por
|
|
operação), registam **exactamente duas** — um tool `<domínio>-read` e um tool `<domínio>-write` —
|
|
cada uma com o schema `{ operation?: string, arguments?: object }`. Chamar sem `operation`
|
|
devolve um catálogo de descoberta (`{ mode, operations: [{operation, description, ...}] }`);
|
|
chamar com `operation` despacha para um executor interno. Isto é o MESMO padrão de "compact tool
|
|
mode" que o dispatcher global de 3 tools usa ao nível do servidor inteiro (`docs/00`, §4) —
|
|
aqui aplicado ao nível de CADA integração de terceiros, mantendo o catálogo total de abilities
|
|
pequeno mesmo com dezenas de operações internas por plugin.
|
|
|
|
Duas implementações distintas deste padrão coexistem no código:
|
|
|
|
1. **ACF e Meta Box** — cada classe implementa o dispatcher "à mão" (método `dispatch()`
|
|
privado + `operations()` que devolve o mapa nome→{mode,run,perm,desc,...}). Não há classe
|
|
base partilhada entre ACF e Meta Box.
|
|
2. **Forms e SEO** — têm uma classe base abstracta partilhada por TODAS as integrações da
|
|
categoria (`EMCP_Tools_Form_Integration`, `EMCP_Tools_SEO_Integration`), da qual CF7 e Slim
|
|
SEO (Free) e os 8+6 plugins Pro (não presentes nesta árvore) herdam. A base implementa
|
|
`register()`, `dispatch()`, o catálogo de descoberta, o schema `{operation,arguments}` e uma
|
|
gate de confirmação genérica (`confirm: true` obrigatório em `arguments` para operações
|
|
marcadas `confirm=>true` no mapa de operações — mecanismo de "são irreversíveis" ainda por
|
|
usar em CF7/Slim SEO Free, mas presumivelmente usado pelos plugins Pro com delete de
|
|
entradas/redirects).
|
|
|
|
**Nuance de segurança repetida nas 4 classes**: o `permission_callback` registado no MCP (a
|
|
"porta grossa" que decide se o tool sequer aparece/corre) é sempre uma capacidade **larga**
|
|
(`edit_posts` para leitura; para escrita varia — ver tabelas abaixo). A gate **fina e real** por
|
|
operação corre DENTRO do `dispatch()`, comparando com o `perm` específico de cada operação do
|
|
mapa `operations()`. Um utilizador pode assim ver o tool `acf-write` listado mas ser recusado
|
|
(`forbidden`) ao tentar `create-field-group` se só tiver `edit_posts` e não `manage_options`.
|
|
|
|
## 1. ACF (Advanced Custom Fields / ACF PRO)
|
|
|
|
**Ficheiro:** `includes/abilities/class-acf-abilities.php` (1627 linhas — a maior classe de
|
|
abilities do plugin). **Classe:** `EMCP_Tools_ACF_Abilities`. **Desde:** 3.2.1.
|
|
|
|
**Condição de registo** (`class-ability-registrar.php`):
|
|
```php
|
|
// ACF abilities — only when Advanced Custom Fields (free or Pro) is active.
|
|
if ( class_exists( 'EMCP_Tools_ACF_Abilities' ) && EMCP_Tools_ACF_Abilities::acf_active() ) {
|
|
$acf = new EMCP_Tools_ACF_Abilities();
|
|
$acf->register();
|
|
...
|
|
}
|
|
```
|
|
`acf_active()` = `function_exists('acf_get_field_groups')` (ACF free OU Pro, qualquer versão
|
|
recente). Sem gate de licença Pro do EMCP em si — funciona com ACF free.
|
|
|
|
### 1.1 Abilities MCP registadas (2)
|
|
|
|
| Ability | Input schema | Descrição | `permission_callback` | readonly/destructive |
|
|
|---|---|---|---|---|
|
|
| `emcp-tools/acf-read` | `{ operation?: string, arguments?: object }` | Lê dados ACF: field groups, valores de campos, options pages, e (ACF 6.1+) CPTs/taxonomias geridos por ACF. Sem `operation` → catálogo. | `check_read_permission()` = `current_user_can('edit_posts')` | readonly=true, destructive=false, idempotent=true |
|
|
| `emcp-tools/acf-write` | `{ operation?: string, arguments?: object }` | Escreve dados ACF (valores, field groups, CPTs/taxonomias). **Escritas desligadas por omissão** (activar em Tools → Plugins → ACF). Sem `operation` → catálogo. | `check_read_permission()` — **nota**: a gate registada no MCP é a mesma de leitura; a gate real de escrita (`manage_options`/`edit_post`) corre por operação dentro de `dispatch()` | readonly=false, destructive=false, idempotent=false |
|
|
|
|
### 1.2 As 15 operações internas (via `operation` + `arguments`)
|
|
|
|
| operação | modo | `perm` (capability) | requer ACF 6.1+? | O que faz |
|
|
|---|---|---|---|---|
|
|
| `list-field-groups` | read | `check_read_permission` (`edit_posts`) | não | Lista field groups: `key, id, title, active, local, field_count`. Args opcionais: `post_id` (filtra por contexto), `search`, `active_only` (default true). |
|
|
| `get-field-group` | read | `check_read_permission` | não | Devolve um field group completo: `location` rules + árvore recursiva de campos (`sub_fields`/`layouts` até profundidade 10). Args: `{ key }` (key ou ID numérico). |
|
|
| `list-options-pages` | read | `check_read_permission` | não | Lista options pages ACF (feature PRO; vazio em ACF free). Sem args. |
|
|
| `get-fields` | read | `check_fields_permission` (`edit_post`/`manage_options` conforme alvo) | não | Lê valores de campos de um post ou options page. Args: `{ post_id }` OU `{ options_page }`; opcionais `{ fields: string[] }` (filtra), `{ include_field_objects: bool }` (envolve valor com type/label). |
|
|
| `list-post-types` | read | `check_manage_permission` (`manage_options`) | **sim** | Lista CPTs geridos por ACF (`acf-post-type` CPT interno). |
|
|
| `get-post-type` | read | `check_manage_permission` | **sim** | Devolve um CPT ACF completo por `{ key }`. |
|
|
| `list-taxonomies` | read | `check_manage_permission` | **sim** | Lista taxonomias geridas por ACF. |
|
|
| `get-taxonomy` | read | `check_manage_permission` | **sim** | Devolve uma taxonomia ACF completa por `{ key }`. |
|
|
| `update-fields` | write | `check_fields_permission` | não | Escreve valores de campos (incl. linhas repeater/flexible/gallery) num post ou options page. Args: `{ post_id\|options_page, fields: {name: value} }`. Campos de tipo PRO (repeater/flexible_content/gallery/clone) recusados se ACF não for Pro. Linhas de `flexible_content` validadas contra `acf_fc_layout` conhecido. Regista before-image no Change Ledger (`EMCP_Tools_Change_Recorder::record_acf_fields`) se essa classe existir. |
|
|
| `create-field-group` | write | `check_manage_permission` | não | Cria um field group com campos + location rules via `acf_import_field_group()` (persiste no CPT `acf-field-group`; NÃO usa `acf_add_local_field_group()`, que seria só memória). Args: `{ title, fields: [...], location?: [[...]] }`. |
|
|
| `update-field-group` | write | `check_manage_permission` | não | Edita um field group **guardado em BD**: título, location, active, position, adiciona novos campos (`add_fields`), altera settings mutáveis de campos existentes (`update_fields`). **Recusa** grupos `local` (registados por acf-json/PHP) e qualquer alteração a `name`/`type` de um campo existente (imutáveis — evitaria orfanar postmeta). Sem delete. |
|
|
| `create-acf-post-type` | write | `check_manage_permission` | **sim** | Regista um CPT via ACF (dados, sem código) usando `acf_import_post_type()`. Args: `{ post_type, title, singular?, public?, hierarchical?, show_in_rest?, supports?, has_archive?, taxonomies? }`. Slug validado contra 21 slugs reservados do WordPress (`post`, `page`, `attachment`, etc.). |
|
|
| `update-acf-post-type` | write | `check_manage_permission` | **sim** | Edita um CPT ACF existente por `{ key }`. **Slug imutável** (recusa se `post_type` no input difere do actual — orfanaria conteúdo). |
|
|
| `create-acf-taxonomy` | write | `check_manage_permission` | **sim** | Regista uma taxonomia via ACF. Args: `{ taxonomy, title, object_type: string[] (post types), singular?, hierarchical?, public?, show_in_rest? }`. |
|
|
| `update-acf-taxonomy` | write | `check_manage_permission` | **sim** | Edita uma taxonomia ACF existente por `{ key }`. **Slug imutável** (orfanaria termos). |
|
|
|
|
`cpt_tax_supported()` = `function_exists('acf_get_acf_post_types') && acf_get_acf_taxonomies && acf_import_post_type && acf_import_taxonomy` (ACF 6.1+). Operações CPT/tax são omitidas do catálogo de descoberta e recusam com `acf_cpt_tax_unsupported` se a versão for anterior.
|
|
|
|
### 1.3 Decisões de design não óbvias (ACF)
|
|
|
|
- **Escrita sempre por `field_key`, nunca por `name`**: comentário no código — `update_field()` por nome falha silenciosamente em alvos que ainda não têm valor guardado para o campo. O resolvedor de campo (`resolve_field()`) tenta: (1) prefixo `field_` directo via `acf_get_field()`, (2) índice por nome/key construído a partir dos field groups aplicáveis ao alvo, (3) fallback `get_field_object()` para alvos options sem valor guardado ainda.
|
|
- **`update_field()` não confia no valor de retorno**: devolve `false` também num no-op (valor idêntico), por isso o sucesso é confirmado por uma releitura (`get_field()`) após cada escrita, não pelo booleano.
|
|
- **Índice de campos por request, invalidado após escrita**: `field_index_cache` (array associativo por `target`) evita relistar field groups a cada operação, mas é explicitamente apagado (`unset`) após `update-fields`/`create-field-group`/`update-field-group` para uma leitura subsequente no mesmo request ver o estado fresco.
|
|
- **Options pages sem lookup reverso**: ACF não tem forma de mapear um alvo `options` de volta aos seus field groups; o índice cai para (a) todo o field group com QUALQUER regra `location` de tipo `options_page`, mais (b) `get_field_objects()` para capturar campos já guardados mesmo sem regra de localização detectável.
|
|
- **Normalização de valores**: `WP_Post`/`WP_User`/`WP_Term` viram resumos compactos (`{id, title/name, ...}`); arrays de imagem/ficheiro ACF (`return_format=array`) reduzidos a `{id, url, alt, mime}` em vez do array completo (que inclui `sizes` com dezenas de entradas).
|
|
- **Sem delete, em lado nenhum**: nem field groups, nem campos, nem valores, nem CPTs/taxonomias — write-only-forward. Confirma o padrão documentado no cabeçalho do ficheiro: "there is deliberately NO delete tool".
|
|
|
|
## 2. Meta Box (metabox.io)
|
|
|
|
**Ficheiro:** `includes/abilities/class-metabox-abilities.php`. **Classe:**
|
|
`EMCP_Tools_Meta_Box_Abilities`. **Desde:** 3.4.2.
|
|
|
|
**Condição de registo:**
|
|
```php
|
|
// Meta Box abilities — only when Meta Box (free or extensions) is active.
|
|
if ( class_exists( 'EMCP_Tools_Meta_Box_Abilities' ) && EMCP_Tools_Meta_Box_Abilities::metabox_active() ) {
|
|
$metabox = new EMCP_Tools_Meta_Box_Abilities();
|
|
$metabox->register();
|
|
...
|
|
}
|
|
```
|
|
`metabox_active()` = `defined('RWMB_VER') && function_exists('rwmb_get_registry')`.
|
|
|
|
### 2.1 Abilities MCP registadas (2)
|
|
|
|
| Ability | Input schema | Descrição | `permission_callback` | readonly/destructive |
|
|
|---|---|---|---|---|
|
|
| `emcp-tools/metabox-read` | `{ operation?, arguments? }` | Lê field groups Meta Box registados, as suas definições de campo, e valores de campo num post/objecto. | `check_read_permission()` = `edit_posts` | readonly=true, idempotent=true |
|
|
| `emcp-tools/metabox-write` | `{ operation?, arguments? }` | Escreve valores de campo Meta Box. **Desligado por omissão** (Tools → Plugins → Meta Box). | `check_read_permission()` — mesma nota que ACF: gate real é por operação | readonly=false, destructive=false, idempotent=false |
|
|
|
|
### 2.2 As 4 operações internas
|
|
|
|
| operação | modo | Descrição |
|
|
|---|---|---|
|
|
| `list-field-groups` | read | Lista meta boxes registados: `id, title, object_type, post_types[], field_count`. Args opcionais: `search`, `object_type`. |
|
|
| `get-field-group` | read | Um meta box completo por `{ id }`: título, object_type, post_types, árvore de campos recursiva (campos `clone`/group aninhados até profundidade 10). |
|
|
| `get-fields` | read | Lê valores via `rwmb_meta()`. Args: `{ post_id }` OU `{ object_type, object_id }`; opcionais `{ fields: string[], include_field_settings: bool }`. |
|
|
| `update-fields` | write | Escreve valores via `rwmb_set_meta()` (não devolve nada — confirmação por releitura). Args: `{ post_id\|object_type+object_id, fields: {id: value} }`. |
|
|
|
|
Ambas as operações de leitura/escrita têm o mesmo `perm`: `check_read_permission`/`check_fields_permission` — sem distinção read/write op-a-op como no ACF (só há uma operação de escrita).
|
|
|
|
### 2.3 Diferenças-chave face ao ACF
|
|
|
|
- **Meta Box free core não tem UI de construção de campos** — campos são declarados em PHP via o filtro `rwmb_meta_boxes`. Por isso NÃO HÁ authoring de field groups/CPT/taxonomia aqui (ao contrário do ACF) — só leitura de definições e leitura/escrita de VALORES.
|
|
- **Nomenclatura invertida face ao ACF**: no Meta Box, o `id` de um campo É a meta key (equivalente ao `name` do ACF), e `name` é o label humano (equivalente ao `label` do ACF) — nota explícita no cabeçalho do ficheiro por ser uma fonte comum de confusão ao portar lógica entre as duas integrações.
|
|
- **`applicable_fields()`**: para alvos `post`, só considera meta boxes cujo `post_types` inclui o tipo do post concreto (Meta Box permite meta boxes restritos a post types específicos); para outros `object_type`, aceita todas as meta boxes desse tipo sem mais filtragem.
|
|
- **Normalização de valor**: reconhece o formato específico de imagem/ficheiro do Meta Box — array associativo com `ID`+`url` E (`full_url` OU `path` OU `mime_type`) — distinto da assinatura ACF (`ID`+`url`+`mime_type`).
|
|
- **`resolve_target()` tem um caso de normalização subtil**: `{ object_type:'post', object_id:N }` sem `post_id` é reescrito internamente para o caminho `post_id` (garante o mesmo 404-check e gate `edit_post` que `{post_id:N}` receberia diretamente).
|
|
|
|
## 3. Forms — infra-estrutura genérica (`EMCP_Tools_Form_Integration`)
|
|
|
|
**Ficheiro:** `includes/abilities/forms/class-form-integration.php`. **Classe:** classe base
|
|
abstracta `EMCP_Tools_Form_Integration`. **Desde:** 3.5.0. Directório `forms/` no Free contém
|
|
apenas ESTE ficheiro + `class-cf7-integration.php` — nenhum outro ficheiro de plugin de
|
|
formulário existe nesta árvore (confirmado por listagem directa, secção 6).
|
|
|
|
### 3.1 Contrato abstracto
|
|
|
|
Cada integração concreta implementa:
|
|
- `id(): string` — id curto, usado para construir os nomes dos tools (`<id>-read`/`<id>-write`).
|
|
- `label(): string` — label humano.
|
|
- `is_active(): bool` — se o plugin de formulários alvo está activo.
|
|
- `operations(): array<string,array>` — mapa `nome => { mode, run, perm, desc, confirm? }`.
|
|
|
|
`is_available()` (usado pelo registrar para decidir se regista) = `is_active()` por omissão.
|
|
|
|
### 3.2 O que a base fornece a TODAS as integrações de formulários
|
|
|
|
- **`register()`** — regista os dois tools `<id>-read`/`<id>-write` com o schema partilhado
|
|
`{ operation?, arguments? }`. Meta annotations do tool write: `readonly=false, destructive=true,
|
|
idempotent=false` — nota: **destructive=true por omissão na base**, diferente de ACF/Meta
|
|
Box/SEO (que marcam `destructive=false`) — reflecte que integrações Pro de formulários
|
|
provavelmente apagam entradas/submissões, uma operação genuinamente destrutiva que CF7 (sem
|
|
armazenamento de submissões) nunca exerce.
|
|
- **`can_read()`** = `edit_posts`; **`can_write()`** = `manage_options` — ambas as gates coarse
|
|
registadas no MCP; a gate real corre por operação via `$op['perm']`.
|
|
- **`dispatch()`** — resolve `operation`, verifica `is_active()` (senão `plugin_inactive`, HTTP 409),
|
|
valida a operação existe no modo certo (`unknown_operation`, HTTP 404), corre `$op['perm']`
|
|
(`forbidden`, HTTP 403), e — **gate de confirmação genérica**: se `$op['confirm']===true`,
|
|
exige `arguments.confirm===true` (senão `confirmation_required`, HTTP 400); o campo `confirm`
|
|
é removido de `$args` antes de chamar o executor real (não polui o input do handler).
|
|
- **Sem mecanismo de change-ledger automático** — ao contrário da base SEO (secção 4), a base
|
|
Forms não regista snapshots before-image; qualquer undo teria de ser implementado por
|
|
integração concreta.
|
|
|
|
## 4. Contact Form 7 (integração Free com ficheiro dedicado)
|
|
|
|
**Ficheiro:** `includes/abilities/forms/class-cf7-integration.php`. **Classe:**
|
|
`EMCP_Tools_CF7_Integration extends EMCP_Tools_Form_Integration`. **Desde:** 3.5.0.
|
|
|
|
**Condição de registo:**
|
|
```php
|
|
if ( class_exists( 'EMCP_Tools_CF7_Integration' ) ) {
|
|
$form_integrations[] = new EMCP_Tools_CF7_Integration();
|
|
}
|
|
... // depois, para todos os $form_integrations:
|
|
foreach ( $form_integrations as $form_integration ) {
|
|
if ( $form_integration->is_available() ) {
|
|
$form_integration->register();
|
|
...
|
|
}
|
|
}
|
|
```
|
|
`is_active()` = `class_exists('WPCF7_ContactForm') || defined('WPCF7_VERSION')`. Nenhum gate de
|
|
licença Pro do EMCP — CF7 é a ÚNICA integração de formulários incluída no build Free.
|
|
|
|
Verificado contra Contact Form 7 6.1.6 (comentário no cabeçalho do ficheiro lista a superfície
|
|
API usada: `WPCF7_ContactForm::find()`, `::get_instance()`, `->id()/->name()/->title()`,
|
|
`->scan_form_tags()`, `->prop()`, `->set_properties()+->save()`).
|
|
|
|
### 4.1 Abilities MCP registadas (2, via a base)
|
|
|
|
| Ability | Descrição gerada (`label().' Read/Write'`) | `permission_callback` |
|
|
|---|---|---|
|
|
| `emcp-tools/cf7-read` | "Contact Form 7, read operations. Call with no operation to list them." | `can_read()` = `edit_posts` |
|
|
| `emcp-tools/cf7-write` | "Contact Form 7, write operations..." | `can_write()` = `manage_options` |
|
|
|
|
### 4.2 As 7 operações internas
|
|
|
|
| operação | modo | `perm` (capability CF7 real) | Descrição |
|
|
|---|---|---|---|
|
|
| `list-forms` | read | `wpcf7_read_contact_forms` (≈`edit_posts`) | Lista todos os formulários CF7: `id, title, slug, field_count`. |
|
|
| `get-form` | read | idem | Um formulário completo por `{ form_id }`: `fields[], mail, mail_2, messages, additional_settings`. |
|
|
| `list-notifications` | read | idem | Os dois templates de email (`mail`, `mail_2`) por `{ form_id }`. |
|
|
| `get-settings` | read | idem | `messages` + `additional_settings` por `{ form_id }`. |
|
|
| `update-notification` | write | `wpcf7_edit_contact_forms` (≈`publish_pages`) | Actualiza um template de email por merge (não substitui): `{ form_id, notification: "mail"\|"mail_2", mail: {subject?, sender?, recipient?, body?, additional_headers?, attachments?, use_html?, active?} }`. |
|
|
| `update-messages` | write | idem | Actualiza mensagens de validação/resposta por merge: `{ form_id, messages: {key: value} }`. |
|
|
| `update-form-settings` | write | idem | Substitui por completo o bloco Additional Settings: `{ form_id, additional_settings: string }`. |
|
|
|
|
**Nota**: os `perm` reais aqui são MAIS finos que a gate coarse da base (`can_write()=manage_options`)
|
|
— usam as capacidades nativas do CF7 (`wpcf7_edit_contact_forms`), que por omissão mapeiam para
|
|
`publish_pages`, não `manage_options`. Um editor sem `manage_options` mas com `publish_pages` é
|
|
assim recusado pela gate coarse do MCP (`can_write`) ANTES sequer de chegar à gate fina do CF7 —
|
|
uma limitação prática: a capability CF7 nativa nunca é realmente exercida nesta implementação
|
|
porque a gate MCP é mais restritiva.
|
|
|
|
**CF7 não guarda submissões** (sem addon Flamingo/DB próprio) — por isso não há operações de
|
|
"entries" (ao contrário do que os 8 plugins Pro de formulários presumivelmente expõem, dado
|
|
todos guardarem submissões nativamente).
|
|
|
|
## 5. SEO — infra-estrutura genérica (`EMCP_Tools_SEO_Integration`)
|
|
|
|
**Ficheiro:** `includes/abilities/seo/class-seo-integration.php`. **Classe:** base abstracta
|
|
`EMCP_Tools_SEO_Integration`. **Desde:** 3.5.0. Directório `seo/` no Free contém apenas ESTE
|
|
ficheiro + `class-slimseo-integration.php`.
|
|
|
|
Distinção documentada no cabeçalho: isto é **diferente** do toolkit Pro SEO & Accessibility
|
|
(`audit-page-seo`/`generate-meta-tags`, ver doc 01/02), que ANALISA e GERA em vez de
|
|
ler/escrever os dados que um plugin de SEO já guarda.
|
|
|
|
### 5.1 Contrato abstracto
|
|
|
|
Mesma forma que Forms: `id()`, `label()`, `is_active()`, `operations()`. Diferenças de
|
|
comportamento da base face a Forms:
|
|
|
|
- **`can_read()`** = `edit_posts`; **`can_write()`** = `edit_posts` (não `manage_options` —
|
|
escrever SEO de um post é uma operação de editor normal, não administrativa).
|
|
- **Meta annotations do tool write**: `readonly=false, destructive=false, idempotent=false` —
|
|
ao contrário de Forms, aqui `destructive=false` por omissão (escritas de metadados SEO nunca
|
|
apagam dados irrecuperáveis por si mesmas).
|
|
- **Change-ledger automático (feature exclusiva da base SEO, ausente em Forms/ACF/Meta Box)**:
|
|
para qualquer operação de modo `write` cujos `arguments` incluam `post_id` ou `term_id`, a base
|
|
chama `recordable_meta_keys($object)` (que cada integração concreta sobrepõe — devolve as meta
|
|
keys que ela escreve para esse tipo de objecto; vazio por omissão = sem registo automático),
|
|
tira um `snapshot_meta()` (before-image dessas keys) ANTES de correr o executor, e — se o
|
|
resultado não for `WP_Error` e `EMCP_Tools_Change_Log` existir — grava uma entrada
|
|
`{domain:'seo', action:'update', target:'<id>:<object>:<obj_id>', rollback:{type:'meta-before-image', object, id, before}}`.
|
|
Isto dá rollback automático a TODAS as integrações SEO que declarem as suas meta keys, sem cada
|
|
uma ter de implementar a lógica de undo.
|
|
|
|
### 5.2 Abilities MCP e a mesma gate de confirmação
|
|
|
|
A `dispatch()` reutiliza a MESMA forma de Forms: catálogo sem `operation`, `plugin_inactive`
|
|
(409) se `is_active()` falhar, `unknown_operation` (404), `forbidden` (403) por `$op['perm']`,
|
|
`confirmation_required` (400) se `$op['confirm']===true` e `arguments.confirm` não for `true`.
|
|
|
|
## 6. Slim SEO (integração Free com ficheiro dedicado)
|
|
|
|
**Ficheiro:** `includes/abilities/seo/class-slimseo-integration.php`. **Classe:**
|
|
`EMCP_Tools_SlimSEO_Integration extends EMCP_Tools_SEO_Integration`. **Desde:** 3.5.0.
|
|
|
|
**Condição de registo:**
|
|
```php
|
|
if ( class_exists( 'EMCP_Tools_SlimSEO_Integration' ) ) {
|
|
$seo_integrations[] = new EMCP_Tools_SlimSEO_Integration();
|
|
}
|
|
... // depois, para todos os $seo_integrations, mesma forma que Forms:
|
|
foreach ( $seo_integrations as $seo_integration ) {
|
|
if ( $seo_integration->is_available() ) { $seo_integration->register(); ... }
|
|
}
|
|
```
|
|
`is_active()` = `defined('SLIM_SEO_VER')`. Slim SEO é a ÚNICA integração de SEO incluída no build
|
|
Free. Armazenamento confirmado ao vivo (comentário "Verified live" no cabeçalho): um único array
|
|
meta `slim_seo` por post/termo (chaves: `title, description, canonical, noindex, nofollow,
|
|
facebook_image, twitter_image`), mais uma option `slim_seo` para as definições do site.
|
|
|
|
### 6.1 Abilities MCP registadas (2, via a base)
|
|
|
|
| Ability | `permission_callback` |
|
|
|---|---|
|
|
| `emcp-tools/slimseo-read` | `can_read()` = `edit_posts` |
|
|
| `emcp-tools/slimseo-write` | `can_write()` = `edit_posts` |
|
|
|
|
### 6.2 As 5 operações internas
|
|
|
|
| operação | modo | `perm` | Descrição |
|
|
|---|---|---|---|
|
|
| `get-post-seo` | read | `edit_posts` | Metadados SEO de um post por `{ post_id }`: `title, description, canonical, noindex, nofollow, og_image, twitter_image` (view unificada — ver mapeamento abaixo). |
|
|
| `get-term-seo` | read | `edit_posts` | Idem para `{ term_id }`. |
|
|
| `get-settings` | read | `manage_options` | Definições SEO do site (a option `slim_seo`). |
|
|
| `update-post-seo` | write | `edit_posts` | Actualiza por merge campo-a-campo: `{ post_id, title?, description?, canonical?, noindex?, nofollow?, og_image?, twitter_image? }`. |
|
|
| `update-term-seo` | write | `edit_posts` | Idem para `{ term_id, ... }`. |
|
|
|
|
`recordable_meta_keys()` sobreposto para devolver `['slim_seo']` (a única meta key, para
|
|
qualquer `$object`) — activa o registo automático no Change Ledger da base para AMBAS as
|
|
operações de escrita.
|
|
|
|
### 6.3 Camada de mapeamento field↔meta-key
|
|
|
|
A classe mantém um `map(): array<string,string>` que traduz o vocabulário unificado do EMCP
|
|
(`title, description, canonical, noindex, nofollow, og_image, twitter_image`) para as chaves
|
|
reais do array `slim_seo` do plugin (`title, description, canonical, noindex, nofollow,
|
|
**facebook_image**, twitter_image` — nota: só `og_image`→`facebook_image` difere). `read_view()`
|
|
e `apply()` usam este mapa nos dois sentidos; `noindex`/`nofollow` são forçados a booleano
|
|
(`!empty()`), o resto passa por `(string)` quando escalar.
|
|
|
|
## 7. WooCommerce e demais integrações Pro-only — não presentes neste build
|
|
|
|
O `class-ability-registrar.php` referencia 18 classes de integração via `class_exists()` que
|
|
**não existem em nenhum ficheiro desta árvore Free** — confirmado por listagem directa de
|
|
`includes/abilities/` (topo), `includes/abilities/forms/`, `includes/abilities/seo/`, e
|
|
`includes/` (topo, todas as subpastas), sem grep recursivo disponível nesta sessão (as
|
|
ferramentas `glob`/`grep` não recursam paths `ssh://`; a confirmação foi feita listando cada
|
|
directório candidato e comparando os nomes de ficheiro presentes contra os nomes de classe
|
|
referenciados). Nenhum destes ficheiros existe nesta instalação — não há schemas nem
|
|
comportamento a documentar; só a metadata de gating, copiada verbatim do registrar.
|
|
|
|
| Classe (referência no registrar) | Plugin de terceiros integrado | Condição de gating exacta (copiada do registrar) |
|
|
|---|---|---|
|
|
| `EMCP_Tools_Woo_Integration` | WooCommerce | `class_exists( 'EMCP_Tools_Woo_Integration' ) && EMCP_Tools_Woo_Integration::woo_active()` — comentário no código: "WooCommerce abilities (Pro) — only when WooCommerce is active." |
|
|
| `EMCP_Tools_WPForms_Integration` | WPForms | `function_exists('emcp_tools_fs') && emcp_tools_fs()->can_use_premium_code()` (licença Pro) **E** `class_exists($classe)` **E** depois `$integration->is_available()` (= `is_active()`, plugin alvo activo) — comentário: "CF7 is free; the five [sic, na prática oito] entry-storing plugins are Pro. Each registers only when its plugin is active." |
|
|
| `EMCP_Tools_GravityForms_Integration` | Gravity Forms | idem (mesmo loop, mesma tripla gate: licença Pro + ficheiro presente + plugin activo) |
|
|
| `EMCP_Tools_FluentForms_Integration` | Fluent Forms | idem |
|
|
| `EMCP_Tools_NinjaForms_Integration` | Ninja Forms | idem |
|
|
| `EMCP_Tools_Formidable_Integration` | Formidable Forms | idem |
|
|
| `EMCP_Tools_MetForm_Integration` | MetForm | idem |
|
|
| `EMCP_Tools_SureForms_Integration` | SureForms | idem |
|
|
| `EMCP_Tools_Forminator_Integration` | Forminator | idem |
|
|
| `EMCP_Tools_Yoast_Integration` | Yoast SEO | `function_exists('emcp_tools_fs') && emcp_tools_fs()->can_use_premium_code()` **E** `class_exists($classe)` **E** `$integration->is_available()` — comentário: "Slim SEO is free; the other 6 are Pro. Each registers only when its SEO plugin is active." |
|
|
| `EMCP_Tools_RankMath_Integration` | Rank Math | idem |
|
|
| `EMCP_Tools_AIOSEO_Integration` | All in One SEO | idem |
|
|
| `EMCP_Tools_SeoPress_Integration` | SEOPress | idem |
|
|
| `EMCP_Tools_SEOFramework_Integration` | The SEO Framework | idem |
|
|
| `EMCP_Tools_SureRank_Integration` | SureRank | idem |
|
|
| `EMCP_Tools_EssentialAddons_Integration` | Essential Addons for Elementor | Só `class_exists( 'EMCP_Tools_EssentialAddons_Integration' )` **E** `$addon_pack->is_available()` — **sem** o gate explícito `emcp_tools_fs()->can_use_premium_code()` no laço (ao contrário de Forms/SEO); comentário: "Elementor addon widget packs (Pro). Each pack contributes ONE read tool for discovery + curation; widgets are placed with the generic add-free-widget tool, so there is deliberately no write tool here." O ficheiro simplesmente não existe fora do build Pro, logo `class_exists()` é sempre falso no Free independentemente de licença. |
|
|
| `EMCP_Tools_PremiumAddons_Integration` | Premium Addons for Elementor | idem (mesmo laço, mesma ausência de gate de licença explícito no registrar) |
|
|
| `EMCP_Tools_UAE_Integration` | Ultimate Addons for Elementor (anteriormente Header Footer Elementor) | `class_exists( 'EMCP_Tools_UAE_Integration' )` **E** `$emcp_uae->is_available()` — comentário: "Ultimate Addons for Elementor (Pro)... Both a widget pack AND a data plugin, so unlike the pure packs it keeps the house read/write dispatcher pair: discovery + templates on read, templates on write." |
|
|
|
|
**Nota adicional confirmada pelo registrar** (fora do escopo directo desta tarefa mas
|
|
adjacente): outras integrações de "Themes-tab" também Pro-only e ausentes desta árvore —
|
|
`EMCP_Tools_GeneratePress_Integration`, `EMCP_Tools_GenerateBlocks_Integration`,
|
|
`EMCP_Tools_Blocksy_Blocks_Integration`, `EMCP_Tools_Blocksy_Extensions_Integration` — todas só
|
|
`class_exists()` + `is_available()`, sem gate de licença explícito no laço (mesmo padrão de
|
|
EssentialAddons/PremiumAddons/UAE). Não fazem parte do escopo pedido (ACF/MetaBox/Forms/SEO/Woo)
|
|
mas são citadas aqui por confirmarem o padrão: **todas** as integrações Pro deste plugin usam
|
|
`class_exists()` simples como gate primário — a distinção Free/Pro é feita por PRESENÇA DE
|
|
FICHEIRO (a pasta `emcp-pro/` inclui-os, `emcp-tools/` não), não por uma verificação de licença
|
|
em runtime dentro de cada laço de registo individual (excepto forms/SEO, que adicionam
|
|
explicitamente `emcp_tools_fs()->can_use_premium_code()` como segunda camada — provavelmente
|
|
porque esses 14 ficheiros de facto existem no zip Pro mas o acesso à FUNCIONALIDADE ainda
|
|
depende de licença activa, ao contrário dos addon-packs/UAE, que talvez sejam gratuitos dentro do
|
|
próprio Pro sem sub-gate de licença adicional).
|
|
|
|
## 8. Blueprint para réplica
|
|
|
|
**Copiar quase 1:1:**
|
|
- **O padrão de dispatcher de dois braços** (`<domínio>-read`/`<domínio>-write`, catálogo sem
|
|
`operation`, despacho por `operation`+`arguments`) — é a peça que mantém o catálogo total de
|
|
tools pequeno com dezenas de integrações possíveis. Vale a pena ter DUAS variantes conforme a
|
|
complexidade: dispatcher "à mão" para integrações com estado rico e reaproveitamento pesado
|
|
(ACF, Meta Box — cada uma tem a sua própria noção de "target"/"field index"), e uma classe base
|
|
abstracta partilhada para famílias homogéneas de integrações simples-mas-numerosas (Forms, SEO)
|
|
onde cada concreta só define `operations()` + os `run` callables.
|
|
- **A gate de confirmação genérica da base** (`confirm: true` obrigatório em `arguments` para
|
|
operações marcadas `confirm=>true`) — mecanismo barato e reutilizável para qualquer operação
|
|
irreversível futura (delete de entrada de formulário, delete de redirect, etc.), sem cada
|
|
integração ter de reimplementar a checagem.
|
|
- **O change-ledger automático da base SEO** (`recordable_meta_keys()` + snapshot before-image +
|
|
`EMCP_Tools_Change_Log::record()`) é o exemplo mais elegante deste documento: dá rollback
|
|
automático a qualquer integração que declare as suas meta keys, com ZERO código extra por
|
|
integração concreta. **Recomendação forte**: subir este padrão para a base Forms também (hoje
|
|
ausente lá) e generalizar para ACF/Meta Box (hoje cada uma implementa o seu próprio snapshot
|
|
ad-hoc — ACF via `EMCP_Tools_Change_Recorder::record_acf_fields`, Meta Box sem qualquer
|
|
registo). Um único mecanismo de "snapshot de meta keys antes de escrever" partilhado por TODAS
|
|
as integrações de terceiros pouparia manutenção e cobriria o gap do Meta Box.
|
|
- **Escrita sempre por identificador estável** (ACF: sempre por `field_key`, nunca por `name`) —
|
|
é uma lição de bug real ("silently fails on targets that have no stored value yet"), replicar
|
|
esta disciplina em qualquer integração própria que tenha um par nome-humano/chave-estável.
|
|
- **Imutabilidade de slug/tipo em updates** (ACF post-type/taxonomy slug; CF7 sem rename de
|
|
formulário) — recusar explicitamente mudanças que orfanariam dados existentes é barato e evita
|
|
uma classe inteira de bugs de integridade referencial silenciosa.
|
|
|
|
**Simplificar:**
|
|
- **O gate duplo de licença Pro em Forms/SEO** (`can_use_premium_code()` + `class_exists()` +
|
|
`is_available()`) é redundante para uma réplica que não tem modelo de negócio Free/Pro — uma
|
|
reescrita própria só precisa da terceira camada (`is_active()`/plugin alvo instalado).
|
|
- **A normalização de valores por tipo de objecto WP** (`WP_Post`/`WP_User`/`WP_Term` →
|
|
resumos compactos) está duplicada quase verbatim entre ACF e Meta Box (`normalize_value()`
|
|
em cada classe, com pequenas variações de forma de imagem/ficheiro). Vale a pena extrair um
|
|
helper único partilhado (`EMCP_Tools_Value_Normalizer::normalize($value)`) parametrizável por
|
|
"assinatura de imagem/ficheiro" (ACF: `ID+url+mime_type`; Meta Box: `ID+url` + uma de
|
|
`full_url`/`path`/`mime_type`) em vez de reimplementar a árvore recursiva duas vezes.
|
|
|
|
**Deixar de fora (ou adiar bastante):**
|
|
- **CPT/taxonomia "managed by ACF"** (ACF 6.1+) é uma feature de nicho (a maioria dos sites
|
|
regista CPTs por código, não pela UI do ACF) — só vale a pena replicar se o público-alvo da
|
|
réplica usar isto activamente. É também a parte mais frágil (7 das 15 operações ACF, gate por
|
|
versão, slugs reservados) — maior superfície de manutenção por unidade de valor entregue.
|
|
- **Options pages ACF** — feature exclusivamente Pro do plugin de terceiros; sem ACF Pro
|
|
instalado esta parte do código é sempre um array vazio. Só vale a pena manter o "stub" que
|
|
devolve `{pro: false, pages: []}` graciosamente, não investir em lógica adicional.
|
|
|
|
**Risco/gotcha não óbvio encontrado no código (citação directa):**
|
|
- `class-ability-registrar.php`, comentário no `try/catch` de `register_all()`: "Ability
|
|
registration runs on every admin page load and every REST request, so an exception here is a
|
|
site-wide fatal: wp-admin becomes unreachable... (issue #100, where a host malware scanner had
|
|
quarantined one of our class files, leaving `require_once` satisfied but the class undeclared).
|
|
No single tool group is worth locking an admin out of their own site." — **implicação directa
|
|
para as 5 integrações deste documento**: cada bloco de registo (ACF, Meta Box, cada form/SEO
|
|
integration) corre dentro deste `try/catch` amplo a nível de `register_groups()`; um erro fatal
|
|
ao construir QUALQUER uma destas classes (ex. uma versão incompatível de ACF que remova uma
|
|
função que o código assume existir) não derruba o site — simplesmente essa família de tools
|
|
fica ausente do catálogo, silenciosamente, com log via `error_log`. Uma réplica DEVE envolver o
|
|
registo de cada grupo de integração de terceiros no mesmo tipo de guarda — nunca deixar uma
|
|
dependência externa instável poder tirar o wp-admin do ar.
|
|
- ACF `execute_update_fields()`: comentário explícito sobre a armadilha de escrever por `name`
|
|
em vez de `key` — já citado acima, mas vale reforçar como o exemplo mais concreto de "testámos
|
|
em produção e partiu" que este documento encontrou.
|
|
- Meta Box `resolve_target()`: a normalização silenciosa de `{object_type:'post', object_id:N}`
|
|
para o caminho `post_id` é subtil — sem ela, alguém podia contornar o `edit_post` gate passando
|
|
o post por `object_type`+`object_id` em vez de `post_id` directo (dois caminhos de input para o
|
|
mesmo alvo, só um gated correctamente, se não fosse esta normalização).
|
|
|
|
## Fonte
|
|
|
|
Leitura directa (19-08-2026) de:
|
|
`includes/abilities/class-acf-abilities.php` (1627 linhas, completo),
|
|
`includes/abilities/class-metabox-abilities.php` (completo),
|
|
`includes/abilities/forms/class-form-integration.php` (completo),
|
|
`includes/abilities/forms/class-cf7-integration.php` (completo),
|
|
`includes/abilities/seo/class-seo-integration.php` (completo),
|
|
`includes/abilities/seo/class-slimseo-integration.php` (completo),
|
|
`includes/abilities/class-ability-registrar.php` (completo, 480+ linhas — todo o `register_groups()`).
|
|
Listagem directa (não grep recursivo, indisponível para paths `ssh://` nesta sessão) dos
|
|
directórios `includes/abilities/`, `includes/abilities/forms/`, `includes/abilities/seo/` e
|
|
`includes/` (topo, todas as subpastas) para confirmar a AUSÊNCIA física de qualquer ficheiro das
|
|
18 classes Pro-only listadas na secção 7. Cruzado com `docs/00-ARQUITECTURA.md` (mesma sessão de
|
|
documentação) para o contrato de `emcp_tools_register_ability()` e o padrão de dispatcher de
|
|
compact tool mode ao nível do servidor.
|