Files
emcp-tools-mapping/docs/10-MODULOS-E-INVENTARIO-PRO.md
T
Claude Code 8ada367bd0 docs: mapeamento completo do EMCP Tools (11 docs, ~6070 linhas)
Especificação funcional do plugin emcp-tools v3.12.1 (msrbuilds/elementor-mcp,
GPL-2.0-or-later) via leitura directa do código-fonte em emanuelalmeida.pt.

- 00: arquitectura (bootstrap, ability registrar, dispatcher, MCP adapter)
- 01: Elementor classico (paginas, layout, widgets, templates, globals)
- 02: Elementor Atomic v4 + Gutenberg
- 03: WordPress core (conteudo, media, settings, temas)
- 04: Themer (CPT, condicoes, render, PHP templates)
- 05: Redirects + change ledger unificado (rollback)
- 06: Sandbox PHP snippets + custom widgets
- 07: Filesystem/DB/WP-CLI/Security/Performance (maior risco)
- 08: Integracoes terceiros (ACF, Meta Box, forms, SEO)
- 09: Stock images + Cloud + OAuth
- 10: Sistema de modulos + inventario Pro-only (30 classes)
- INDEX: sintese, sequencia de construcao, tabela de risco

Produzido por 10 subagentes code-explorer em paralelo + revisao cruzada de
consistencia. Cada doc inclui blueprint de replica (copiar/simplificar/omitir).
2026-08-19 06:41:04 +01:00

662 lines
44 KiB
Markdown

# 10 — Sistema de módulos (Modules tab) e inventário Pro-only (metadata)
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. Cruzado com `docs/00-ARQUITECTURA.md` (mesma batch) e `skill://emcp-tools`.
Este documento tem duas partes independentes: **Parte 1** documenta o sistema de "módulos" — a
camada de toggles on/off do Modules tab, que é ortogonal ao sistema de abilities/MCP tools
(uma ability pode existir sempre, só um módulo; um módulo pode não ter nenhuma ability associada;
ver Themer/Redirect para o padrão onde módulo E abilities coexistem com gating cruzado). **Parte 2**
é o inventário definitivo — para cada classe referenciada com `class_exists()` em
`class-ability-registrar.php::register_groups()` que não seja confirmada Free por outro documento
desta batch, confirma-se aqui, por leitura directa (listagem de directório + tentativa de leitura
de ficheiro), se existe ou não na árvore Free instalada.
---
# PARTE 1 — Sistema de módulos
## 1.1 Classe base — `EMCP_Tools_Module`
Ficheiro: `includes/modules/class-module.php` (~100 linhas). Classe abstracta; contrato mínimo
que todo módulo tem de implementar:
| Método | Abstracto? | Contrato |
|---|---|---|
| `id(): string` | sim | id estável (`a-z0-9-`) — usado como valor no array `emcp_tools_active_modules` e como infixo das option keys próprias do módulo |
| `title(): string` | sim | título humano para o card no Modules tab |
| `description(): string` | sim | descrição de uma linha para o card |
| `tier(): string` | sim | `'free'` \| `'pro'` — controla o badge de tier na UI |
| `default_active(): bool` | sim | se o módulo arranca activo por omissão (seeded uma única vez, ver §1.2) |
| `register(): void` | sim | liga os hooks do módulo. Só chamado pela registry quando activo **e** disponível |
| `render_settings(): void` | sim | desenha os knobs do módulo dentro do seu card (mostrado quando activo) |
| `is_available(): bool` | não (default `true`) | sonda de dependência/capacidade — override para gate em features do servidor (ex.: suporte WebP) ou em licença Pro |
| `settings_fields(): array` | não (default `[]`) | mapa `option_key => ['type','default','sanitize_callback']` para registo sanitizado no grupo de settings do módulo |
| `is_active(): bool` | concreto | `in_array($this->id(), get_option('emcp_tools_active_modules', []), true)` |
| `settings_group(): string` | concreto | `'emcp_tools_module_' . str_replace('-','_',$this->id()) . '_settings'` — grupo Settings API próprio, para o form do módulo gravar independentemente dos toggles de outros módulos |
| `has_settings(): bool` | concreto | `[] !== $this->settings_fields()` |
| `settings_url(): string` | não (default `''`) | quando definido, o card mostra um link "Configure →" para uma página admin dedicada, em vez de um overlay inline |
Constante: `OPTION_ACTIVE = 'emcp_tools_active_modules'` — a ÚNICA option que guarda quais módulos
estão activos (array de ids).
## 1.2 Registry — `EMCP_Tools_Modules_Registry`
Ficheiro: `includes/modules/class-modules-registry.php` (~115 linhas). Singleton
(`instance()` / `reset_for_tests()` para testes).
- **`register(EMCP_Tools_Module $module)`** — idempotente por id (`$this->modules[$module->id()] = $module`).
- **`all()` / `get($id)` / `active()`** — leitura simples; `active()` filtra pelos que têm `is_active()===true`.
- **`apply_defaults()`** — mecanismo de seeding. Option `emcp_tools_modules_seeded` (const `OPTION_SEEDED`)
guarda uma lista de ids **já considerados** para seeding (não um booleano único). Para cada módulo
registado ainda não na lista `seeded`: marca-o como seeded e, se `default_active()===true` e ainda
não estiver em `active`, adiciona-o a `active`. Grava as duas options só se algo mudou.
**Porquê lista por-módulo e não um marcador booleano:** um módulo novo introduzido numa versão
posterior do plugin é seeded no load seguinte sem re-seedar — ou remover — o que o utilizador já
tinha alterado manualmente nos módulos existentes. É o desenho correcto para migração aditiva sem
tocar em estado do utilizador.
- **`boot_active()`** — chamado em `init` (presume-se prioridade 5, confirmado pelos comentários nos
módulos Redirect/Themer/Agent-Skills que dizem "abilities register on wp_abilities_api_init, antes
do módulo arrancar em init:5"). Para cada módulo em `active()`: se `is_available()===true`, chama
`register()`.
**Consequência de desenho importante:** os grupos de abilities MCP de um módulo (quando existem)
**não podem depender de `register()` ter corrido**, porque `wp_abilities_api_init` dispara ANTES de
`init:5`. Por isso todo módulo com abilities associadas expõe um **helper estático** `is_enabled()`
(lê `get_option(OPTION_ACTIVE)` directamente, sem depender da instância da registry) que o
`class-ability-registrar.php` chama em vez de `$module->is_active()`. Ver Redirect, Cloud, Themer,
Agent-Skills, Image-Optimization (`module_is_active()`), Memory, Migrate — todos seguem este padrão.
## 1.3 Os 7 módulos "tab-only" / leves
Todos vivem em `includes/modules/class-{id}-module.php`. Cinco são efectivamente free
(Prompts, Brand Kits, Redirect, Cloud, Themer); dois têm o **ficheiro de metadata presente na
árvore Free mas `tier()==='pro'`** (Templates, Agent Skills) — um padrão deliberado do autor,
explicitado no comentário do código de Agent Skills: "This class carries no Pro logic and lives
safely in the free tree, like EMCP_Tools_Templates_Module."
### Prompts (`id: 'prompts'`)
| Campo | Valor |
|---|---|
| `tier()` | `free` |
| `default_active()` | `true` |
| `is_available()` | não sobreposto (sempre `true`) |
| `settings_url()` | `admin.php?page={PAGE_SLUG}-prompts` |
| `register()` | **no-op** |
Feature "tab-only": o módulo só controla se a tab admin Prompts (e o respectivo stat card) aparece;
quem lê `is_active()` é a classe de admin para mostrar/esconder a tab. O conteúdo free/Pro DENTRO
da tab (amostras bundled vs biblioteca premium) é inalterado por este toggle — é outra camada de
gating, não deste módulo.
### Brand Kits (`id: 'brand-kits'`)
Idêntico em estrutura ao Prompts: `tier()=free`, `default_active()=true`, `register()` no-op,
`settings_url()` aponta para `-brand-kits`. Controla apenas a visibilidade da tab; os 10 kits
bundled free (`EMCP_Tools_Free_Brand_Kits`, §1.6) vs 50+ kits Pro por licença são geridos por outro
mecanismo, não por este toggle.
### Templates (`id: 'templates'`) — **tier Pro, ficheiro em Free**
| Campo | Valor |
|---|---|
| `tier()` | **`pro`** |
| `default_active()` | `true` |
| `is_available()` | **sobreposto**: `function_exists('emcp_tools_fs') && emcp_tools_fs()->can_use_premium_code()` |
| `settings_url()` | `admin.php?page={PAGE_SLUG}-templates` |
| `register()` | **no-op** |
Comentário do autor no ficheiro: "Pro tier — free users see a locked card in Modules and the
standalone tab is hidden. [...] This class is metadata only (no Pro logic), so it lives safely in
the free tree." Ou seja: o ficheiro `.php` desta classe existe fisicamente no build Free (por isso
não aparece na Parte 2 como "ausente"), mas `is_available()` bloqueia sempre que não há licença
activa — o resultado prático é indistinguível de um módulo Pro-only, só a UI "locked card" difere.
### Redirect Manager (`id: 'redirects'`, const `ID`)
| Campo | Valor |
|---|---|
| `tier()` | `free` |
| `default_active()` | `true` |
| `settings_url()` | `admin.php?page={PAGE_SLUG}-redirects` |
| `is_enabled(): bool` (estático) | lê `OPTION_ACTIVE` directamente — usado pelo registrar |
| `register()` | `EMCP_Tools_Redirect_Store::init()` (instala tabela em `init:20`) + `EMCP_Tools_Redirect_Handler::init()` (handler 301/302 no front-end), ambos condicionais a `class_exists()` |
Comentário do autor: desactivar este módulo é um **verdadeiro kill switch** — o handler de
redirect pára, as MCP tools caem (gated no registrar via `is_enabled()`), a tab admin esconde-se, e
delete/rename deixa de emitir sugestões de redirect. Ver Doc05 para `Redirect_Abilities`/
`Redirect_Store`/`Redirect_Handler` em detalhe.
### EMCP Cloud (`id: 'cloud'`)
| Campo | Valor |
|---|---|
| `tier()` | `free` |
| `default_active()` | `true` |
| `settings_url()` | `admin.php?page=emcp-tools-connection#emcp-conn-main` |
| `is_enabled(): bool` (estático) | idem padrão Redirect |
| `register()` | `EMCP_Tools_Cloud_Connect::init()` (arranca o fluxo admin do cliente OAuth) |
O grupo `EMCP_Tools_Cloud_Abilities` (ficheiro `includes/abilities/class-cloud-abilities.php`,
**confirmado presente na árvore Free**) só regista quando **ambas** as condições são verdadeiras:
`EMCP_Tools_Cloud_Module::is_enabled()` **e** `EMCP_Tools_Cloud::is_connected()` — isto é, o gate
não é licença, é estado de ligação (o site tem de estar ligado a uma conta EMCP Cloud). Toda a
infra-estrutura de suporte (`includes/cloud/`: `class-cloud.php`, `class-cloud-client.php`,
`class-cloud-connect.php`, `class-cloud-http.php`, `class-cloud-sync.php`,
`class-gateway-credential.php`, `class-settings-sync.php`) está presente e é free. Ver Doc09.
### Themer (`id: 'themer'`)
| Campo | Valor |
|---|---|
| `tier()` | `free` |
| `default_active()` | `true` |
| `settings_url()` | `edit.php?post_type={Themer_CPT::POST_TYPE}` |
| `is_enabled(): bool` (estático) | idem padrão Redirect/Cloud |
| `register()` | ver abaixo — o mais rico dos 7 módulos leves |
`register()` (chamado só em `init:5`, activo+disponível):
1. `(new EMCP_Tools_Themer_CPT())->register()` — regista o CPT.
2. Se `class_exists('EMCP_Tools_Themer_HFE_Conflict')`: `::init()` — aviso de conflito com o Header
Footer Elementor (que constrói os mesmos slots header/footer); até o admin escolher um sistema,
Themer ganha deterministicamente.
3. `EMCP_Tools_Themer_Index::register_hooks()` — hooks de rebuild do índice de condições.
4. **Heal one-time**: se `get_option('emcp_tools_themer_index_healed') !== '1'`, chama
`EMCP_Tools_Themer_Index::rebuild()` e grava o marcador. **Comentário do autor, citado**: "a prior
build could leave the condition index empty (the rebuild raced the metabox meta writes), so
existing templates silently stopped applying." É a reparação de uma race condition real de dados
já corrompidos em sites existentes — o marcador de option garante que corre exactamente uma vez
por upgrade, sem o admin ter de voltar a gravar cada template manualmente.
5. Se `! is_admin()`: `(new EMCP_Tools_Themer_Render_Controller())->init()` (front-end).
6. Se `is_admin() && class_exists('EMCP_Tools_Themer_Metabox')`: `(new EMCP_Tools_Themer_Metabox())->init()`.
7. Se `class_exists('EMCP_Tools_Themer_Blocks')`: `::init()` — blocos Gutenberg dinâmicos.
8. Se `class_exists('EMCP_Tools_Themer_Widgets')`: `::init()` — widgets clássicos dinâmicos.
9. Se `class_exists('EMCP_Tools_Themer_PHP')`: `(new EMCP_Tools_Themer_PHP())->init()` — feature de
templates PHP puro (confirmado presente: `includes/themer/php/class-themer-php.php`). Nota: o
grupo de abilities `EMCP_Tools_Themer_PHP_Abilities` (também confirmado presente) tem o SEU
PRÓPRIO toggle independente (`EMCP_Tools_Themer_PHP::enabled()`), separado do toggle base do
Themer — ver Doc04.
Comentário do autor: desactivar o módulo pára o CPT, a tomada de controlo do front-end e a tab; o
registrar omite as tools também — kill switch total. Ver Doc04 para detalhe completo do subsistema
`includes/themer/`.
### Agent Skills (`id: 'agent-skills'`) — **tier Pro, ficheiro em Free**
| Campo | Valor |
|---|---|
| `tier()` | **`pro`** |
| `default_active()` | `true` |
| `is_available()` | **sobreposto**: `function_exists('emcp_tools_fs') && emcp_tools_fs()->can_use_premium_code()` |
| `settings_url()` | `admin.php?page={PAGE_SLUG}-skills` |
| `register()` | **no-op** |
| `is_enabled(): bool` (estático) | idem padrão Redirect/Cloud/Themer, usado por DOIS consumidores |
Controla a exposição em **runtime** das skills bundled a agentes de IA ligados: as tools MCP
`list-skills` / `get-skill` e o catálogo `## Skills` injectado no contexto de discovery. Desligar
remove ambos (e o footprint de ~900 tokens da injecção) **sem tocar** no caminho de
instalação-local na tab Skills (i.e. skills já descarregadas para disco continuam lá, só deixam de
ser expostas a agentes MCP).
Dois consumidores estáticos de `is_enabled()`:
1. `class-ability-registrar.php` — gate de `EMCP_Tools_Skill_Abilities` (confirmado **ausente** da
árvore Free, ver Parte 2).
2. `EMCP_Tools_Skill_Catalog::discovery_catalog()` — gate da injecção do catálogo no contexto MCP.
**Nuance importante:** o ficheiro desta classe **existe** no build Free instalado (contraste com
Memory/Migrate, cujos módulos nem sequer têm ficheiro na árvore Free — ver Parte 2 §2.2). É por
isso que `EMCP_Tools_Agent_Skills_Module` NÃO aparece na tabela final de "classes ausentes" — mas
funcionalmente comporta-se como Pro, porque `is_available()` exige licença e a única ability que
depende dela (`Skill_Abilities`) está de qualquer forma ausente do ficheiro-sistema Free.
## 1.4 Image Optimization — o módulo opt-in mais substancial (deep dive)
Ficheiros em `includes/modules/image-optimization/`: `class-image-optimization-module.php`,
`class-image-optimizer.php`, `class-image-resizer.php`, `class-webp-generator.php`,
`class-webp-rewriter.php`, `class-bulk-optimizer.php`, `settings-fields.php`.
### `EMCP_Tools_Image_Optimization_Module` (id `'image-optimization'`, const `ID`; option prefix `PREFIX = 'emcp_tools_module_image_optimization_'`)
| Campo | Valor |
|---|---|
| `tier()` | `free` |
| `default_active()` | **`false`** — único dos 9 módulos (contando os 2 de deep-dive) que é **opt-in por omissão**, além do SVG Support |
| `is_available()` | **sobreposto**: `(new EMCP_Tools_Webp_Generator(82))->is_available()` — delega para `wp_image_editor_supports(['mime_type'=>'image/webp'])`; se o editor de imagem do servidor (GD/Imagick) não sabe exportar WebP, o módulo não fica disponível de todo |
| `module_is_active(): bool` (estático) | usado pelo registrar para condicionar a ability `resize-media` |
`settings_fields()` — 6 option keys, todas sob `PREFIX`:
| Key | Tipo | Default |
|---|---|---|
| `compress` | bool (`'1'`/`'0'`) | `1` |
| `webp` | bool | `1` |
| `webp_serve` | bool | `1` |
| `quality` | int | `60` |
| `max_dimension` | int | `0` (0 = sem cap) |
| `keep_originals` | bool | `1` |
`current_settings()` resolve estas options num array tipado (quality passa por
`EMCP_Tools_Image_Optimizer::clamp_quality()` — clamp 1-100 partilhado, single source of truth).
`register()` (chamado só activo+disponível):
- Se `compress` OU `webp`: instancia `EMCP_Tools_Image_Optimizer($settings)`, hook em
`wp_generate_attachment_metadata` (prioridade 20) → `on_generate_metadata()`.
- Se `webp`: `(new EMCP_Tools_Webp_Rewriter($settings['webp_serve']))->register()`.
- Se `is_admin()`: `(new EMCP_Tools_Bulk_Optimizer($settings))->register()`.
`render_settings()` faz `include` de `settings-fields.php` com `$settings` em scope (view partial
pura: switches para compress/webp/webp_serve, slider 1-100 para quality, number input para
max_dimension, switch para keep_originals; usa um closure local `$emcp_io_toggle` para DRY do HTML).
### `EMCP_Tools_Image_Optimizer` — pipeline compress-on-upload
- Const `META_KEY = '_emcp_optim'` — meta do post attachment onde fica o resultado.
- Construtor normaliza settings (bool/bool/int-clamp/int/bool).
- `clamp_quality(int): int` (estático) — clamp 1-100, partilhado com `Image_Resizer`.
- `sizes_to_process(metadata, basedir): string[]` — caminhos absolutos do full-size + todos os
sub-sizes gerados.
- `backup_path(file, upload): string` (estático) — espelha o caminho relativo às uploads sob
`uploads/emcp-originals/`.
- `should_skip(optim): bool` — `true` se `optim['status']==='done'` (idempotência).
- `on_generate_metadata($metadata, $attachment_id)` — o hook callback:
1. guarda: `$metadata` tem de ser array; settings tem de ter `compress` OU `webp`;
2. **`apply_filters('emcp_tools_optimize_attachment', true, $attachment_id)`** — opt-out
per-attachment. Usado por `sideload-image`/`add-stock-image` quando o chamador passa
`convert_webp:false` (conversão a dar timeout em shared hosting) — é o ÚNICO ponto de
extensibilidade externo deste pipeline;
3. mime tem de ser `image/jpeg` ou `image/png` (WebP e GIF **não** entram no pipeline de
compressão — só os dois formatos de origem processados);
4. se já `status=done` no meta existente, salta;
5. resolve `wp_upload_dir()`, calcula `sizes_to_process()`, chama `process_files()`, grava o
resultado em `_emcp_optim`.
- `process_files($files, $upload, $full_rel, $basedir): array` — o loop real por ficheiro:
- mede tamanho "antes"; se `keep_originals`, faz backup (skip se já existir — idempotente);
- `wp_get_image_editor()`; `set_quality()`; **só** o full-size (`$file === $full_abs`) recebe o
cap `max_dimension` via `resize($max,$max,false)` (scale-to-fit, nunca crop) — os sub-sizes
NUNCA são redimensionados por este cap;
- se `compress`: `editor->save($file)` (re-encode IN PLACE, mesmo caminho);
- mede tamanho "depois";
- se `webp_ok`: `generator->generate($file)` → sibling;
- devolve agregado: `status=done, original_bytes, optimized_bytes, webp_bytes, backups[], webps[]`.
### `EMCP_Tools_Webp_Generator`
- `sibling_path(file): string` (estático) — `"$file.webp"` (ex.:
`name-800x600.jpg.webp` — a extensão original é **preservada**, `.webp` é anexado como extensão
composta, para o rewriter encontrar deterministicamente sem precisar de índice).
- `is_available()` — `wp_image_editor_supports(['mime_type'=>'image/webp'])`.
- `generate(file)` — skip se sibling já existe (idempotente); `wp_get_image_editor()`;
`set_quality()`; `save($sibling, 'image/webp')`.
### `EMCP_Tools_Webp_Rewriter`
- Construtor: `serve_frontend=true` por omissão; captura `basedir`/`baseurl` de `wp_upload_dir()`.
- `register()` — hooks `wp_get_attachment_url`, `wp_get_attachment_image_src`,
`wp_calculate_image_srcset` (todos prioridade 20).
- `should_rewrite($accept, $is_rest, $serve_frontend): bool` (estático) — **REST/CLI/cron
qualifica-se SEMPRE** (as MCP media tools resolvem sempre para WebP, independentemente do toggle
frontend); frontend requer adicionalmente o toggle `serve_frontend` **e** header
`Accept: image/webp`.
- `webp_url(url): string` (estático) — regex troca `.jpg`/`.jpeg`/`.png` (antes de qualquer query
string) pelo sibling `.webp`; outras extensões passam inalteradas.
- `is_rest_context()` — `REST_REQUEST` const OU `WP_CLI` const OU `wp_doing_cron()`.
- `url_to_path(url)` — mapeia URL de uploads de volta a caminho absoluto, só dentro do `baseurl`
(scoping de segurança).
- `maybe_rewrite(url)` — decisão final por URL: só reescreve se `allowed()` **e** a URL webp
difere **e** o ficheiro `.webp` existe mesmo em disco (nunca devolve uma URL para um ficheiro
inexistente).
### `EMCP_Tools_Bulk_Optimizer` — processador resumível da biblioteca existente
- Consts: `ACTION_BATCH='emcp_tools_optimize_batch'`, `ACTION_RESTORE='emcp_tools_optimize_restore'`,
`NONCE='emcp_tools_modules'`, `OPTION_CURSOR='emcp_tools_module_image_optimization_bulk_cursor'`.
- `register()` — 2 handlers `wp_ajax_*`.
- `batch_size(n): int` (estático) — clamp 1-50, default 10 se `<=0`.
- `progress(total, processed): array` (estático, pura) — `{total,processed,remaining,percent,done}`.
- `ajax_batch()` — nonce + `manage_options`; query de TODOS os attachment IDs jpeg/png ordenados
por ID ASC; lê cursor de `OPTION_CURSOR`; fatia o batch a partir do cursor; para cada ID:
metadata → `sizes_to_process()` → `process_files()` → grava `_emcp_optim`; avança o cursor; reset
do cursor a 0 quando `progress.done`; devolve JSON de progresso.
- `ajax_restore()` — nonce + `manage_options`; percorre `uploads/emcp-originals/` recursivamente
(`RecursiveIteratorIterator`); para cada backup: copia de volta sobre o caminho vivo, apaga o
sibling `.webp` do destino, incrementa `restored`; reset do cursor; devolve `{restored}`.
## 1.5 SVG Support — o módulo com maior superfície de segurança (deep dive)
Ficheiros: `includes/modules/svg-support/class-svg-support-module.php`,
`includes/modules/svg-support/class-svg-sanitizer.php`.
### `EMCP_Tools_SVG_Support_Module` (id `'svg-support'`, const `ID`; prefix `PREFIX = 'emcp_tools_module_svg_support_'`)
| Campo | Valor |
|---|---|
| `tier()` | `free` |
| `default_active()` | **`false`** — opt-in, comentário explícito "a security surface" |
| `is_available()` | **sobreposto**: `EMCP_Tools_SVG_Sanitizer::library_available()` — fail-closed se a biblioteca de sanitização não estiver disponível |
WordPress bloqueia uploads SVG por omissão (SVG é XML e pode transportar scripts). Elementor já
permite SVG para utilizadores autorizados via o seu próprio handling de unfiltered-upload — este
módulo é dirigido a sites **sem** Elementor (ou onde o mime `svg` não está registado por outra via).
`svg_already_supported(): bool` (estático) — verifica `get_allowed_mime_types()` por `'svg'` ou
`'svg|svgz'` — se já suportado por outro plugin/tema, mostra uma nota informativa em
`render_settings()` (o módulo continua a sanitizar de qualquer forma quando activo).
`settings_fields()` — UMA option: `admin_only` (bool, default `'0'`).
`required_capability(): string` — resolve para `manage_options` se `admin_only` estiver ligado,
senão `upload_files`; filtrável via `emcp_tools_svg_upload_capability`.
`register()` liga:
1. `upload_mimes` → `allow_svg_mime()` — adiciona `'svg'=>'image/svg+xml'` só se
`current_user_can(required_capability())`.
2. `wp_check_filetype_and_ext` (prioridade 10, 4 args) → `fix_svg_filetype()` — **"the piece most
SVG plugins miss"** (comentário do autor, citado): a sniff real-content de mime type do
WordPress (via `finfo`) frequentemente reporta `text/plain` ou `image/svg` para SVGs e rejeita o
upload; esta correcção resolve explicitamente `ext`/`type` para ficheiros `.svg` quando o
utilizador tem a capability — é o que faz uploads REST/sideload funcionarem, não só o
`media-new.php` clássico.
3. `wp_handle_upload_prefilter` + `wp_handle_sideload_prefilter` → ambos `sanitize_upload()`.
4. `admin_head` → `media_thumbnail_css()`, só se `is_admin()`.
`sanitize_upload($file)` — o gate real: se a extensão não for `svg`, passa; se o utilizador não
tem a capability, rejeita com mensagem de erro; senão corre `(new EMCP_Tools_SVG_Sanitizer())
->sanitize_file($tmp_name)` — se falhar, rejeita **fail-closed** ("could not be sanitized and was
rejected for security").
`media_thumbnail_css()` — injecção CSS mínima para as thumbnails SVG renderizarem corretamente
dimensionadas na grelha/lista da Media Library.
### `EMCP_Tools_SVG_Sanitizer` — wrapper fino sobre `enshrined/svg-sanitize` (vendorizada)
Mesma biblioteca que o plugin "Safe SVG" usa. SVG é XML — pode transportar script, event handlers,
referências externas e payloads XXE; permitir upload SVG cru sem sanitizar é um vector de
stored-XSS.
`library_available(): bool` — 3 estratégias de carregamento em cascata:
1. classe já carregada (`class_exists('\enshrined\svgSanitize\Sanitizer')`);
2. `EMCP_Tools_Adapter_Bootstrap::ensure()` (**o mesmo mecanismo de preload do Jetpack Autoloader
usado para o MCP adapter vendorizado**, ver `00-ARQUITECTURA.md` §2.1) ou fallback directo a
`vendor/autoload_packages.php`;
3. **fallback próprio**: `register_fallback_autoloader()` — regista um autoloader PSR-4 escopado
directamente contra `vendor/enshrined/svg-sanitize/src/`, para a sanitização SVG continuar a
funcionar "even if the Jetpack classmap wasn't regenerated" (comentário do autor) — redundância
defensiva deliberada contra um modo de falha real de geração de classmap Composer/Jetpack.
`sanitize(string $svg): string|false`:
- instancia `\enshrined\svgSanitize\Sanitizer`;
- `minify(false)`;
- `removeRemoteReferences(true)` — **endurecimento SSRF/XSS explícito** para lá da configuração
por omissão da biblioteca (remove `xlink:href` remoto, `use@href` remoto);
- `false` em vazio/falha.
`sanitize_file(path): bool` — ler → sanitizar → sobrescrever in-place; `false` se ilegível ou
sanitização falhar.
## 1.6 `EMCP_Tools_Free_Brand_Kits` — serviço de suporte (não é módulo)
Ficheiro: `includes/class-free-brand-kits.php` (nível topo de `includes/`, não em `modules/`).
Contraparte free do serviço `EMCP_Tools_Pro_Brand_Kits`. Onde o Pro busca 50+ kits de
`emcptools.com` atrás de licença, este lê um conjunto pequeno e curado embutido no plugin
(`assets/brand-kits/free-brand-kits.json`) — disponível a todos, sem licença, o mesmo modelo dos 5
prompts de amostra bundled.
- `get_bundle(): array` — parse cacheado em memória (`self::$bundle`) do JSON; forma:
`['categories' => [['slug','label','kits' => [{kit}, ...]]]]`. Para cada kit, se existir
`assets/brand-kits/{slug}.svg` (previews pré-renderizadas, fontes outlined), injecta
`thumbnail_url` + `preview.thumbnail_url` com a URL do plugin (o JSON não pode saber a URL do
plugin). **Nunca devolve `WP_Error`** — os dados estão bundled, por isso estão sempre disponíveis
(devolve bundle vazio se o ficheiro faltar).
- `find_kit(kit_slug, category_slug='')` — procura linear.
- `count_kits(): int` — total, para a barra de stats do admin.
**Facto crucial para o blueprint**: esta classe só **PROVIDENCIA** os dados do kit. A **aplicação**
(escrever cores/tipografia no Elementor kit activo) passa sempre pelo
`EMCP_Tools_System_Kit_Writer` PARTILHADO (confirmado presente em `includes/class-system-kit-writer.php`)
e pelo `EMCP_Tools_Kit_Backup_Store` (backups reversíveis, confirmado presente em
`includes/class-kit-backup-store.php`) — **exactamente o mesmo caminho usado pelo Pro**. Ou seja: a
funcionalidade de "aplicar um kit" via UI admin funciona **sem licença nenhuma** (kits free + writer
partilhado); é só a **ferramenta MCP** para o fazer programaticamente
(`EMCP_Tools_System_Kit_Abilities`) que está totalmente ausente do build Free (ver Parte 2).
---
# PARTE 2 — Inventário Pro definitivo
## 2.1 Metodologia
1. Leitura integral de `includes/abilities/class-ability-registrar.php::register_groups()`
(627 linhas) — extracção de TODAS as chamadas `class_exists('EMCP_Tools_...')` que condicionam
o registo de um grupo de abilities ou de um pack de integração.
2. Cruzamento com listagens de directório completas e literais (equivalente a `ls`) de:
- `includes/abilities/` (47 ficheiros `.php` + subpastas `forms/`, `seo/`);
- `includes/abilities/forms/` (2 ficheiros: `class-cf7-integration.php`,
`class-form-integration.php` — só a base + CF7);
- `includes/abilities/seo/` (2 ficheiros: `class-seo-integration.php`,
`class-slimseo-integration.php` — só a base + SlimSEO);
- `includes/modules/` (7 ficheiros de módulo + `class-module.php` + `class-modules-registry.php`
+ subpastas `image-optimization/`, `svg-support/` — **sem** `class-memory-module.php` nem
`class-migrate-module.php`).
3. Para cada classe candidata a Pro-only (i.e. referenciada no registrar mas ausente das listagens
acima), **tentativa directa de leitura** do ficheiro esperado (equivalente a `test -f`) —
20 tentativas, cada uma resultando em `head: impossível abrir '...' para leitura: No such file
or directory` (SSH remoto, `head` a falhar por ausência do ficheiro). Nenhuma presunção — cada
linha da tabela abaixo tem confirmação de ausência por, no mínimo, listagem de directório
completa, e a maioria tem confirmação dupla (listagem + tentativa de leitura directa).
Classes confirmadas **presentes** (portanto free, excluídas desta tabela por já estarem cobertas
noutros documentos da batch): `EMCP_Tools_Image_Resize_Abilities`, `EMCP_Tools_ACF_Abilities`,
`EMCP_Tools_Meta_Box_Abilities`, `EMCP_Tools_CF7_Integration`, `EMCP_Tools_SlimSEO_Integration`,
`EMCP_Tools_Active_Theme_Integration`, `EMCP_Tools_Astra_Integration`,
`EMCP_Tools_Spectra_Integration`, `EMCP_Tools_Kadence_Integration`,
`EMCP_Tools_Kadence_Blocks_Integration`, `EMCP_Tools_PHP_Snippet_Abilities`,
`EMCP_Tools_Sandbox_Cloud_Abilities`, `EMCP_Tools_Cloud_Abilities`, `EMCP_Tools_Cloud_Module`,
`EMCP_Tools_Global_Classes_Abilities`, `EMCP_Tools_Global_Classes_Write_Abilities`,
`EMCP_Tools_Themer_Abilities`, `EMCP_Tools_Themer_Module`, `EMCP_Tools_Themer_PHP_Abilities`,
`EMCP_Tools_Themer_PHP`, `EMCP_Tools_Redirect_Module`, `EMCP_Tools_Redirect_Abilities`,
`EMCP_Tools_Image_Optimization_Module`, `EMCP_Tools_Agent_Skills_Module` (presente mas `tier=pro`,
ver §1.3).
## 2.2 Tabela final — Classes referenciadas no registrar mas ausentes do build Free (Pro-only)
30 classes, agrupadas por família funcional. Coluna "Condição de gating" reproduz literalmente a
lógica de `register_groups()` (nomes de variáveis simplificados por clareza).
| Classe | Grupo funcional | Condição de gating (registrar) | Módulo Pro associado |
|---|---|---|---|
| `EMCP_Tools_Woo_Integration` | Integração WooCommerce (CRUD produtos/encomendas/etc.) | `class_exists(...) && EMCP_Tools_Woo_Integration::woo_active()` | — (gate próprio: plugin WooCommerce activo; sem module toggle dedicado) |
| `EMCP_Tools_WPForms_Integration` | Integração de formulários — leitura de entries (Pro) | `emcp_tools_fs()->can_use_premium_code() && class_exists(...)`, depois `$integration->is_available()` | — (tab "Forms"; CF7 é a única integração free) |
| `EMCP_Tools_GravityForms_Integration` | idem | idem | — |
| `EMCP_Tools_FluentForms_Integration` | idem | idem | — |
| `EMCP_Tools_NinjaForms_Integration` | idem | idem | — |
| `EMCP_Tools_Formidable_Integration` | idem | idem | — |
| `EMCP_Tools_MetForm_Integration` | idem | idem | — |
| `EMCP_Tools_SureForms_Integration` | idem | idem | — |
| `EMCP_Tools_Forminator_Integration` | idem | idem | — |
| `EMCP_Tools_Yoast_Integration` | Integração SEO — leitura/escrita meta SEO (Pro) | `emcp_tools_fs()->can_use_premium_code() && class_exists(...)`, depois `$integration->is_available()` | — (tab "SEO"; SlimSEO é a única integração free) |
| `EMCP_Tools_RankMath_Integration` | idem | idem | — |
| `EMCP_Tools_AIOSEO_Integration` | idem | idem | — |
| `EMCP_Tools_SeoPress_Integration` | idem | idem | — |
| `EMCP_Tools_SEOFramework_Integration` | idem | idem | — |
| `EMCP_Tools_SureRank_Integration` | idem | idem | — |
| `EMCP_Tools_GeneratePress_Integration` | Integração de tema/framework (Pro) | `class_exists(...)` — comentário do código: "classes only present when Pro loaded" — depois `$integration->is_available()` | — |
| `EMCP_Tools_GenerateBlocks_Integration` | idem | idem | — |
| `EMCP_Tools_Blocksy_Blocks_Integration` | Integração Blocksy — blocos | `class_exists(...)`, depois `is_available()` | — |
| `EMCP_Tools_Blocksy_Extensions_Integration` | Integração Blocksy — Companion extensions | `class_exists(...)`, depois `is_available()` | — |
| `EMCP_Tools_EssentialAddons_Integration` | Pack de widgets Elementor de terceiros — Essential Addons (Pro) | `class_exists(...)`, depois `is_available()` ("contributes ONE read tool for discovery + curation") | — |
| `EMCP_Tools_PremiumAddons_Integration` | Pack de widgets Elementor — Premium Addons (Pro) | idem | — |
| `EMCP_Tools_UAE_Integration` | Ultimate Addons for Elementor (ex-Header Footer Elementor) — widget pack + data plugin | `class_exists(...)`, depois `is_available()` — único pack que mantém o par read/write dispatcher (discovery+templates na leitura, templates na escrita) | — |
| `EMCP_Tools_Block_Builder_Abilities` | Construtor de blocos Gutenberg via MCP (Pro) | `class_exists(...)` — comentário: "self-guards on license" — Gutenberg, NÃO gated por Elementor activo | — |
| `EMCP_Tools_System_Kit_Abilities` | Brand Kit / System Kit — ferramenta MCP (Pro) | `class_exists(...)` dentro do bloco `if ($elementor_active)` — "self-guards on license" | Módulos "Brand Kits" (free) e "Templates" (Pro) são metadata-only; a ferramenta MCP para aplicar kits é sempre Pro, mesmo com os 10 kits free disponíveis via UI (§1.6) |
| `EMCP_Tools_Seo_Abilities` | Toolkit SEO on-page — ferramenta MCP (Pro) | idem, dentro de `$elementor_active` | — |
| `EMCP_Tools_A11y_Abilities` | Toolkit de acessibilidade — ferramenta MCP (Pro) | idem | — |
| `EMCP_Tools_Widget_Builder_Abilities` | Construtor de widgets Elementor custom — ferramenta MCP (Pro) | idem | — |
| `EMCP_Tools_Skill_Abilities` | Ferramentas MCP `list-skills`/`get-skill` (Pro) | `class_exists('EMCP_Tools_Skill_Abilities') && class_exists('EMCP_Tools_Agent_Skills_Module') && EMCP_Tools_Agent_Skills_Module::is_enabled()` | **Agent Skills** — módulo PRESENTE na árvore Free (`tier()='pro'`, ver §1.3); a ability em si está sempre ausente independentemente do toggle |
| `EMCP_Tools_Memory_Abilities` | Project Memory — ferramenta MCP (Pro) | `class_exists('EMCP_Tools_Memory_Abilities') && class_exists('EMCP_Tools_Memory_Module') && EMCP_Tools_Memory_Module::is_enabled()` | **Memory** — módulo TAMBÉM ausente da árvore Free (ver linha seguinte) |
| `EMCP_Tools_Memory_Module` | Módulo "Memory" (toggle no Modules tab) | `class_exists('EMCP_Tools_Memory_Module')`, usado em conjunto com `Memory_Abilities` acima | Pro — ao contrário de Agent Skills/Templates, nem o ficheiro de metadata do módulo está na árvore Free; não há card "Memory" a mostrar-se bloqueado na Modules tab de um site Free |
| `EMCP_Tools_Migrate_Abilities` | Backup / Migrate / Sync — ferramenta MCP (Pro) | `class_exists('EMCP_Tools_Migrate_Abilities') && class_exists('EMCP_Tools_Migrate_Module') && EMCP_Tools_Migrate_Module::is_enabled()`. As duas tools destrutivas deste grupo vêm desactivadas por omissão mesmo quando disponíveis. | **Migrate** — módulo TAMBÉM ausente (linha seguinte) |
| `EMCP_Tools_Migrate_Module` | Módulo "Migrate" (toggle no Modules tab) | idem padrão de `Memory_Module` | Pro — mesmo padrão: nem o ficheiro de metadata existe na árvore Free |
**Nota sobre `Memory_Module`/`Migrate_Module` vs `Templates_Module`/`Agent_Skills_Module`:** o
código tem DOIS padrões distintos para features Pro sem equivalente free algum:
- **Padrão "card bloqueado"** (Templates, Agent Skills): o ficheiro `.php` do módulo VIVE na árvore
Free, `tier()==='pro'`, `is_available()` exige licença — o utilizador Free VÊ o card na Modules
tab, mas bloqueado/locked, como incentivo de upsell visível.
- **Padrão "invisível"** (Memory, Migrate): nem o ficheiro do módulo existe no build Free — não há
card nenhum a mostrar-se, a feature é completamente invisível a um utilizador Free até subir para
o build Pro. Só as próprias abilities (também ausentes) e o módulo referenciam estas classes; sem
card na Modules tab não há sequer superfície de descoberta da feature no admin.
---
## Blueprint para réplica
### Sistema de módulos (`class-module.php` + `class-modules-registry.php`)
**Copiar quase verbatim** — é uma pequena máquina de estado limpa (~215 linhas as duas classes
juntas), sem lógica Pro nenhuma. O único detalhe de desenho que vale a pena preservar
deliberadamente é a **lista `seeded` por-módulo** (em vez de um marcador booleano único) em
`apply_defaults()` — é o que permite adicionar um módulo novo numa versão futura sem re-seedar nem
tocar no estado que o utilizador já ajustou nos módulos existentes. E o padrão do **helper estático
`is_enabled()`** em todo módulo com abilities associadas — necessário porque `wp_abilities_api_init`
dispara antes de `init:5`, então o gate das MCP tools nunca pode depender de `register()` já ter
corrido.
### Prompts / Brand Kits (módulos "tab-only")
Sem lógica nenhuma (`register()` no-op) — só interessam se formos replicar a UI de tabs
correspondente. Podem ser omitidos por completo numa primeira réplica sem perda funcional MCP
nenhuma.
### Redirect / Cloud / Themer (módulos-wrapper)
**Vale a pena adoptar o padrão inteiro**, não só o wrapper: um módulo que arranca a infra-estrutura
pesada (store, handler, CPT) em `init:5` E gate as MCP tools associadas via um helper estático
separado é o mecanismo real de "kill switch" — desliga tudo (runtime + tools) a partir de UMA
option, sem desregistar código morto por todo o lado. O heal-on-upgrade do Themer
(`emcp_tools_themer_index_healed`) é um padrão geral a reter: sempre que se corrige um bug de
corrupção de dados, adicionar também um marcador de reparação única para sites JÁ afectados —
corrigir só o código novo deixa instalações existentes permanentemente partidas.
### Templates / Agent Skills (módulos "metadata Pro")
**Ignorar** numa réplica 100% free/open — só fazem sentido se construirmos o nosso próprio sistema
de licenciamento/tiering. Se algum dia quisermos essa separação, o padrão exacto a copiar é:
`tier()` + `is_available()` a checar um SDK de licença (equivalente Freemius), com a classe de
metadata do módulo a viver sempre no build "free" para mostrar um card bloqueado como incentivo de
upsell — decisão de UX deliberada, não acidental (o próprio comentário do autor cita este padrão
explicitamente ao comparar Agent Skills com Templates).
### Image Optimization
**Copiar de perto — é a feature opt-in mais bem desenhada e mais valiosa de toda a árvore Free, sem
nenhuma dependência Pro.** Decisões de desenho a preservar:
- **Idempotência** via marcador de estado `_emcp_optim` em post meta — evita reprocessar em cada
regeneração de metadata.
- **Reversibilidade** via espelho de backup em `uploads/emcp-originals/` — nunca destruir sem saída
de emergência.
- O filtro `emcp_tools_optimize_attachment` como seam de extensibilidade explícito — manter um
ponto de opt-out per-attachment para outras ferramentas (sideload/stock-image) é um padrão a
reter mesmo fora deste contexto específico.
- **REST/CLI sempre-WebP vs frontend condicional ao header `Accept`** é um comportamento subtil mas
importante — as MCP tools devem sempre receber o asset optimizado, independentemente da política
pública de servir WebP do site.
- O processador em lote resumível (cursor em option + batch pequeno, 1-50) é o padrão standard
WP-admin-ajax para evitar timeout em bibliotecas grandes — sem surpresas, replicar tal-e-qual.
- **Gotcha a preservar**: o clamp de quality (1-100) é partilhado entre `Optimizer` e `Resizer` via
método estático único — evitar duplicar essa lógica de clamp em dois sítios.
### SVG Support
**Copiar de perto, com cuidado redobrado — é a feature com maior superfície de segurança de toda a
árvore Free.** Coisas que NÃO se podem saltar:
- **Sanitização fail-closed**: um SVG que não consiga ser limpo tem de ser rejeitado, nunca deixado
passar silenciosamente.
- A correcção de `wp_check_filetype_and_ext` **não é opcional** — a maioria das implementações
ingénuas de "basta adicionar svg a upload_mimes" esquecem-se disto e o upload SVG falha
silenciosamente no caminho REST/sideload (comentário explícito do autor: "the piece most SVG
plugins miss" — é exactamente o tipo de gotcha não óbvio que vale a pena citar e replicar).
- `removeRemoteReferences(true)` explícito — não confiar nos defaults da biblioteca de
sanitização, configurar o endurecimento SSRF/XSS deliberadamente.
- O autoloader PSR-4 escopado como fallback próprio para a biblioteca vendorizada — um padrão de
redundância defensiva a reter sempre que se vendoriza um pacote Composer dentro de um plugin
WordPress (geração de classmap Jetpack/Composer é um footgun real e conhecido).
### Free Brand Kits
Vale a pena copiar o **padrão** (bundle de um dataset JSON+SVG pequeno e gratuito, partilhando o
caminho de escrita/aplicação com qualquer tier pago) mesmo que não cheguemos a construir kits Pro
nós próprios — significa que a UX de "aplicar um kit inicial" via admin funciona sem infra-estrutura
de licenciamento nenhuma.
### Inventário Pro (Parte 2)
Para uma réplica 100% free/aberta, estas 30 classes **simplesmente não se constroem** — representam
~30 dos ~165 tools totais (~18%) e estão inteiramente ausentes do que seria preciso reimplementar
para um clone só-free. Se algum dia quisermos uma separação de monetização própria, o mecanismo
exacto a copiar é: (a) gate `class_exists()` no ponto de chamada do registrar (o ficheiro da classe
Pro literalmente não existe a menos que a pasta do plugin pago esteja activa — mesmo modelo de
Free/Pro como duas pastas de plugin distintas usado por este vendor, ver `00-ARQUITECTURA.md` §7);
(b) um toggle de módulo SECUNDÁRIO e opcional (`is_enabled()`) para o punhado de tools que também
querem um interruptor admin independente da licença (Skill/Memory/Migrate); (c) `is_available()` na
própria classe de módulo a verificar um SDK de licença, para os módulos "metadata-only" com card
bloqueado (Templates/Agent-Skills).
---
## Fonte
Leitura directa (19-08-2026) de:
- `includes/modules/class-module.php`
- `includes/modules/class-modules-registry.php`
- `includes/modules/class-prompts-module.php`
- `includes/modules/class-brand-kits-module.php`
- `includes/modules/class-templates-module.php`
- `includes/modules/class-redirect-module.php`
- `includes/modules/class-cloud-module.php`
- `includes/modules/class-themer-module.php`
- `includes/modules/class-agent-skills-module.php`
- `includes/modules/image-optimization/class-image-optimization-module.php`
- `includes/modules/image-optimization/class-image-optimizer.php`
- `includes/modules/image-optimization/class-image-resizer.php`
- `includes/modules/image-optimization/class-webp-generator.php`
- `includes/modules/image-optimization/class-webp-rewriter.php`
- `includes/modules/image-optimization/class-bulk-optimizer.php`
- `includes/modules/image-optimization/settings-fields.php`
- `includes/modules/svg-support/class-svg-support-module.php`
- `includes/modules/svg-support/class-svg-sanitizer.php`
- `includes/class-free-brand-kits.php`
- `includes/abilities/class-ability-registrar.php` (627 linhas, integral — extracção de todas as
chamadas `class_exists()`)
Listagens de directório completas (equivalente a `ls`), 19-08-2026:
- `includes/abilities/` (47 ficheiros + `forms/`, `seo/`)
- `includes/abilities/forms/`
- `includes/abilities/seo/`
- `includes/modules/` (7 módulos + base + registry + `image-optimization/`, `svg-support/`)
- `includes/modules/image-optimization/`
- `includes/modules/svg-support/`
- `includes/themer/` e `includes/themer/php/`
- `includes/cloud/`
- `includes/` (nível topo)
Tentativas directas de leitura de ficheiro (equivalente a `test -f`), todas devolvendo "No such
file or directory" — confirmação de ausência, 19-08-2026, para: `class-woo-integration.php`,
`class-memory-module.php`, `class-migrate-module.php`, `class-skill-abilities.php`,
`class-memory-abilities.php`, `class-migrate-abilities.php`, `class-system-kit-abilities.php`,
`class-seo-abilities.php`, `class-a11y-abilities.php`, `class-widget-builder-abilities.php`,
`class-block-builder-abilities.php`, `class-essential-addons-integration.php`,
`class-premium-addons-integration.php`, `class-uae-integration.php`,
`class-generatepress-integration.php`, `class-generateblocks-integration.php`,
`class-blocksy-blocks-integration.php`, `class-blocksy-extensions-integration.php`,
`forms/class-wpforms-integration.php`, `seo/class-yoast-integration.php` (spot-checks
representativos das famílias Forms/SEO Pro, cuja ausência integral é também confirmada pela
listagem completa dos respectivos subdirectórios).
Cruzado com `docs/00-ARQUITECTURA.md` (mesma batch, 19-08-2026) e `skill://emcp-tools` (auditoria
de postura de segurança, 16-08-2026).