# 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).