feat(wordpress): EMCP Tools workflow skills + widget catalogs + pending wordpress skills

- emcp-page-building, emcp-content-ops, emcp-site-audit: EMCP Tools MCP
  workflows verified live (atomic/legacy interplay, apply-template overwrite
  risk, create-theme-template goes live immediately, false-positive malware
  pattern in scan-security, change ledger + rollback)
- elementor-pro-widgets: 30+5 native Elementor Pro widgets (curated catalog)
- elementskit-widgets / powerpack-widgets: 42 + 97 third-party widgets,
  widgetType extracted from plugin source (not guessed by convention)
- plugin.json bumped 1.2.0 -> 1.3.0, keywords + description updated
- commits pending wordpress skills already present as untracked files
  (emcp-tools, wordfence, wp-activity-log, seguranca-descomplicar,
  webp-express, wp-fastest-cache, wp-font-perf, wp-meteor,
  wp-activity-log, redis-object-cache, app-for-cloudflare) and pending
  edits (rank-math, wp-cli, wp-content-seo-gate)
This commit is contained in:
Claude Code
2026-08-19 03:44:00 +01:00
parent aa981e8089
commit 4a55d51329
22 changed files with 6432 additions and 29 deletions
+3 -3
View File
@@ -1,12 +1,12 @@
{
"name": "wordpress",
"description": "WordPress development, maintenance and optimization - plugins, themes, WooCommerce, Elementor, Crocoblock. Backed by NotebookLM notebooks.",
"version": "1.2.0",
"description": "WordPress development, maintenance and optimization - plugins, themes, WooCommerce, Elementor, Crocoblock, EMCP Tools MCP (page building, content ops, security/performance audit), Elementor Pro/ElementsKit/PowerPack widget catalogs. Backed by NotebookLM notebooks.",
"version": "1.3.0",
"author": {
"name": "Descomplicar - Crescimento Digital",
"url": "https://descomplicar.pt"
},
"homepage": "https://git.descomplicar.pt/ealmeida/descomplicar-plugins",
"license": "MIT",
"keywords": ["wordpress", "woocommerce", "elementor", "crocoblock", "development", "performance", "licensing"]
"keywords": ["wordpress", "woocommerce", "elementor", "crocoblock", "development", "performance", "licensing", "emcp-tools", "elementskit", "powerpack"]
}
@@ -0,0 +1,651 @@
---
name: app-for-cloudflare
description: Gestão da zona Cloudflare a partir do wp-admin via App for Cloudflare® (`app-for-cf`, Digital Point) — SSL/TLS, Speed, Caching, Network, Scrape Shield, Bot Management, Super Bot Fight Mode, WAF real via Managed Ruleset, HSTS, Certificate Transparency, Leaked Credential Checks, Turnstile CAPTCHA, R2 media storage, Zero Trust Access, Page/Cache Rules, DMARC, Web Analytics, ferramentas de diagnóstico (request trace/IP/domain/WHOIS), purge de cache, guest page caching a nível de edge. Usar quando "app for cloudflare", "cloudflare wordpress", "zona cloudflare", "waf cloudflare", "bot management", "super bot fight mode", "hsts cloudflare", "rocket loader", "certificate transparency", "leaked credential checks", "bots de ia cloudflare", "ai_bots_protection", "is_robots_txt_managed", "managed ruleset", "purge cache cloudflare", "turnstile", "cloudflare r2", "cloudflare access", "page rules cloudflare", "cache rules cloudflare", "dmarc cloudflare", "cfPageCachingSeconds", "cfTurnstile", "app_for_cf", ou qualquer alteração a definições de zona Cloudflare/config do plugin num site do bundle Descomplicar.
---
# /app-for-cloudflare — Gestão de zona Cloudflare via App for Cloudflare®
Plugin `app-for-cf` (Digital Point), versão `1.10.0` (`APP_FOR_CLOUDFLARE_VERSION`),
instalado nos sites do bundle Descomplicar®.
Não é um simples proxy visual — a maior parte das definições de "boas
práticas" que o plugin lista **não vivem em `wp_options`**: são lidas e
escritas directamente na **Cloudflare API v4**, usando um token scoped
guardado numa única opção serializada. Confundir as duas camadas é o erro
mais comum ao operar este plugin.
**Fonte:** `CONFIG-Plugins-Referencia.md` §1 (App for Cloudflare®) e
`BUNDLE-Excelencia-WP.md` §2.2 e §2.4 (GEO/bots de IA) — mapeamento inicial
feito em `emanuelalmeida.pt` (16-08-2026), zona `574b243ebabd8ea2b8d89e68921f52cc`,
plano **Free Website**. Expandido na mesma sessão com leitura completa do
código-fonte (11.309 linhas em 61 ficheiros PHP,
`wp-content/plugins/app-for-cf/`): `Repository/Cloudflare.php` (2178
linhas — `getSettingsToManage()`, `setEasyMode()`, `updateSettings()`),
`Setup.php` (defaults de `wp_options`), `Admin/Template/Settings.php` (766
linhas — UI completa das 6 tabs), `Admin/Base/Admin.php` (estrutura das 14
páginas de menu), `Base/Pub.php` (657 linhas — guest page caching, purge,
preload), `Helper/Api.php` (gate Free/Pro), `Cron/Jobs.php` e
`Cli/PurgeCache.php` (automação de purge), e todos os 14 templates de
página em `Admin/Template/`.
---
## 0. As duas camadas de config — não confundir
| Camada | Onde vive | Como ler/escrever | Exemplos |
|---|---|---|---|
| **Config do plugin** (não é Cloudflare) | `wp_options.app_for_cf`, array PHP serializado, 18 chaves (ver §8) | `wp option get/patch` | `cfZoneId`, `cfAccountId`, `cloudflareAuth.token`, `cfPageCachingSeconds`, `cloudflarePreload`, `cfPurgeCacheOnAdminBar`, `cfTurnstile`, `cfProxy`, `cfR2Bucket` |
| **Config de zona Cloudflare** (as ~52 "boas práticas" + Bot Management + WAF, ver §1) | **Na Cloudflare, não no WordPress** | API v4 (`https://api.cloudflare.com/client/v4`), bearer token = `cloudflareAuth.token` da opção acima | `ssl`, `security_header` (HSTS), `waf`, `bot_management`, `rocket_loader`, `cache_level`, `hotlink_protection`, etc. |
Confirmado por leitura completa do código: **não existe** namespace WP REST
próprio exposto pelo plugin (`app-for-cf` não aparece em `rest_api_init` em
nenhum dos 61 ficheiros PHP) — todas as chamadas de configuração de zona
são directas à Cloudflare API v4 a partir do backend PHP do plugin
(`Api/Cloudflare.php`), usando o token guardado localmente. Para
automatizar fora do wp-admin, replicar essa mesma chamada directa
(`curl`/PHP) em vez de procurar um endpoint local que não existe.
A opção `cloudflareAuth` suporta **dois modos de autenticação** (ver
`Setup::defaults()`): `type: 'token'` (API token scoped, o único usado nos
sites do bundle) ou o modo legado `email` + `api_key` (Global API Key —
não usar, é o modo antigo e menos seguro da Cloudflare, mantido só por
compatibilidade retroativa no plugin).
### Ler a config do plugin (wp-cli)
```bash
PATH=/home/USER/public_html
# Opção completa (JSON)
wp option get app_for_cf --format=json --allow-root --path=$PATH
# Uma sub-chave
wp option pluck app_for_cf cfZoneId --allow-root --path=$PATH
wp option pluck app_for_cf cfPageCachingSeconds --allow-root --path=$PATH
# Editar uma sub-chave sem apagar as restantes (SEMPRE patch, nunca update, em opção serializada)
wp option patch update app_for_cf cfPageCachingSeconds 21600 --allow-root --path=$PATH
wp option patch update app_for_cf cloudflarePreload 1 --allow-root --path=$PATH
```
`cfProxy`, `cfR2Bucket`, `cloudflareAuth`, `cfTurnstile` são sub-arrays —
`wp option patch` só edita um nível; para sub-arrays aninhados usar
`wp eval` com `update_option()` sobre o array completo lido primeiro.
### Extrair o token para chamar a Cloudflare API directamente
```bash
TOKEN=$(wp eval 'echo maybe_unserialize(get_option("app_for_cf"))["cloudflareAuth"]["token"];' --allow-root --path=$PATH)
ZONE=$(wp eval 'echo maybe_unserialize(get_option("app_for_cf"))["cfZoneId"];' --allow-root --path=$PATH)
curl -s -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
"https://api.cloudflare.com/client/v4/zones/$ZONE/settings" | jq .
```
---
## 1. As definições de zona (por categoria) — lista completa do código
Fonte: `Repository/Cloudflare.php::getSettingsToManage()` (linhas 88-721),
a definição autoritativa usada pelo próprio plugin para desenhar a página
de Settings e para `updateSettings()`. **Achado desta expansão:** o número
"44" da auditoria original referia-se à contagem informal mostrada na UI
(que agrupa alguns campos); a contagem real de entradas no array
`getSettingsToManage()` é **52**. Lidas em bloco via `GET
/zones/{zone}/settings` (a maioria) + endpoints individuais para os campos
com `override_endpoint` (`security_header`, `certificate_transparency`,
`leaked_credential_checks`, `bot_management`, `speed_brain`, `fonts`,
`content_converter`, `origin_max_http_version`, `h2_prioritization`,
`settings/nel`, `flags` para `crawlhints`, `argo/tiered_caching`).
Escritas via `PATCH /zones/{zone}/settings/{setting_id}` com `{"value":
...}`, excepto onde a definição tem `overwrite_write_method` diferente
(`PUT` para tudo em `bot_management`, `POST` para
`leaked-credential-checks`/`ct/alerting`/`crawlhints`).
| Categoria | Definições |
|---|---|
| **Topo (fora de secção)** | `development_mode` (bool, bom = `off`), `security_level` (select: `off`/`essentially_off`/`low`/`medium`/`high`/`under_attack`, bom = `essentially_off`) — mostradas no topo da página Settings, não numa tab |
| **SSL/TLS** | `ssl` (modo de encriptação: `off`/`flexible`/`full`/`strict`/`origin_pull`), `always_use_https`, `security_header` (HSTS — ver §3), `min_tls_version` (`1.0`-`1.3`), `opportunistic_encryption`, `tls_1_3` (radio: `off`/`on`/`zrt` — zero round trip), `automatic_https_rewrites`, `ech` (Encrypted Client Hello), `certificate_transparency` (ver §4, `beta`), `tls_client_auth` |
| **Segurança** | `waf` (toggle legado, **deprecated** — ver §2), `bot_fight_mode` (Bot Fight Mode básico — ver §5a), `ai_bots_protection`, `crawler_protection` (ambos geridos via `bot_management` PUT — ver §5a), `bot_likely_automated`, `bot_definitely_automated`, `bot_verified_bots`, `bot_static_resource_protection`, `bot_optimize_wordpress`, `bot_enable_js` (Super Bot Fight Mode — ver §5a), `leaked_credential_checks` (ver §4), `challenge_ttl` (select 5min-12meses, bom = 30min), `browser_check`, `replace_insecure_js` |
| **Speed** | `polish` (select: `off`/`lossless`/`lossy`), `webp`, `speed_brain` (`beta`, substitui Rocket Loader/Mirage antigos), `fonts` (`beta`, Cloudflare Fonts — serve Google Fonts a partir do edge), `early_hints`, `rocket_loader` (deprecated para segurança — ver §2), `content_converter` (`beta`), `http2`, `origin_max_http_version` (int, bom = `2`), `http3`, `h2_prioritization`, `0rtt` (TLS 1.3 zero round trip) |
| **Caching** | `cache_level` (radio: `basic`/`simplified`/`aggressive`, bom = `aggressive`), `browser_cache_ttl` (select 0-12meses, `0` = respeitar headers de origem), `crawlhints` (`beta`, indica a crawlers quando o conteúdo mudou), `always_online`, `tiered_caching` (Argo Tiered Cache) |
| **Network** | `ipv6`, `websockets`, `pseudo_ipv4` (select: `off`/`add_header`/`overwrite_header`), `ip_geolocation`, `max_upload` (select 100-500MB), `nel` (Network Error Logging, `value.enabled` aninhado), `opportunistic_onion` |
| **Scrape Shield** | `email_obfuscation`, `hotlink_protection` |
```bash
# Ler uma definição individual
curl -s -H "Authorization: Bearer $TOKEN" \
"https://api.cloudflare.com/client/v4/zones/$ZONE/settings/rocket_loader" | jq .
# Escrever uma definição individual (formato standard "value")
curl -s -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
"https://api.cloudflare.com/client/v4/zones/$ZONE/settings/rocket_loader" \
-d '{"value":"off"}' | jq .
```
`webp`, `content_converter`, `speed_brain`, `fonts`, `crawlhints`,
`tiered_caching` e outros podem estar **plan-locked** (não editáveis no
plano Free Website) — a API devolve o valor actual mas ignora tentativas
de escrita; não é bug, é limite de plano. A UI do plugin marca isto
lendo `editable` do resultado da API (ver `override_result_editable_if_has`
no código) e desactiva o `<select>`/checkbox correspondente.
### EasyConfig — o que o botão "Easy config" aplica em bloco
`Repository\Cloudflare::setEasyMode()` (linha 723) aplica de uma vez, via
`POST /zones/{zone}/settings` (bulk) + 5 chamadas individuais:
```
0rtt=on, browser_cache_ttl=0, cache_level=aggressive, early_hints=on,
http3=on, ip_geolocation=on, ipv6=on, min_tls_version=1.2,
opportunistic_encryption=on, opportunistic_onion=on, pseudo_ipv4=off,
rocket_loader=off, tls_1_3=zrt, websockets=on,
security_level=essentially_off (fora do WordPress; 'medium' fora do XF)
+ settings/nel → {"value":{"enabled":false}}
+ settings/origin_max_http_version → {"value":"2"}
+ settings/speed_brain → {"value":"on"}
+ settings/fonts → {"value":"on"}
+ argo/tiered_caching → {"value":"on"}
```
Não toca em SSL/TLS (excepto `min_tls_version`/`opportunistic_encryption`),
Bot Management, WAF, HSTS nem Scrape Shield — o EasyConfig é só Speed +
Caching + Network básico. Preferir aplicar definição a definição (§7 desta
skill) para manter controlo e rasto de decisão, como já recomendado no
achado original.
---
## 2. WAF real — o toggle `waf` está morto, usar Managed Ruleset
O toggle legado `waf` está **deprecated para zonas novas**: `PATCH
/zones/{id}/settings/waf` devolve `1027 WAF is deprecated for this zone`
independentemente do valor enviado. O mecanismo real de protecção é a fase
de ruleset `http_request_firewall_managed`, que **pode estar completamente
vazia** (zero regras, zero protecção) mesmo num site "protegido pelo
Cloudflare" — foi o estado encontrado em `emanuelalmeida.pt` antes da
correcção.
```bash
# 1. Confirmar se a fase managed já tem entrypoint com regras
curl -s -H "Authorization: Bearer $TOKEN" \
"https://api.cloudflare.com/client/v4/zones/$ZONE/rulesets/phases/http_request_firewall_managed/entrypoint" | jq .
# Vazio/erro "could not find entrypoint" = zero protecção real, apesar do que o toggle `waf` sugere
# 2. Implantar o Cloudflare Managed Free Ruleset na fase managed
curl -s -X PUT -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
"https://api.cloudflare.com/client/v4/zones/$ZONE/rulesets/phases/http_request_firewall_managed/entrypoint" \
-d '{
"rules": [
{
"action": "execute",
"action_parameters": {
"id": "77454fe2d30c4220b5701f6fdfb893ba"
},
"expression": "true",
"description": "Cloudflare Managed Free Ruleset"
}
]
}' | jq .
```
`77454fe2d30c4220b5701f6fdfb893ba` é o ID do **Cloudflare Managed Free
Ruleset** — disponível em qualquer plano, incluindo Free Website. Testar
sempre com um pedido real (POST/AJAX contra o site) depois de implantar,
não confiar só no `200 OK` do PUT.
A fase `http_request_firewall_custom` (regras próprias, não geridas) fica
tipicamente vazia (`10003 could not find entrypoint`) num site sem regras
custom — não é erro, é "zero regras próprias, só o Managed Ruleset". O
plugin (versão Pro) gere regras custom através da página **Firewall**
(§10) — "User agents" e "IP addresses" são interfaces amigáveis para criar
regras Custom Rules na fase `http_request_firewall_custom` sem precisar de
escrever expressões Cloudflare manualmente.
**Rocket Loader não substitui isto.** `rocket_loader` é uma definição de
**Speed** (adia/reordena `<script>` no browser), não de segurança — o
próprio plugin marca-o `not_good_alert` (recomenda desligado). Testado
nesta sessão como hipótese de causa de CLS elevado num site: desligar não
resolveu o CLS, mas manter desligado continua a ser a recomendação do
plugin por outras razões (risco de quebra em sites com jQuery/Elementor).
`speed_brain` é o sucessor recomendado pela Cloudflare (substitui Rocket
Loader e Mirage), mas está marcado `beta` no código do plugin.
---
## 3. HSTS (`security_header`)
```bash
curl -s -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
"https://api.cloudflare.com/client/v4/zones/$ZONE/settings/security_header" \
-d '{
"value": {
"strict_transport_security": {
"enabled": true,
"max_age": 15552000,
"include_subdomains": true,
"preload": false,
"nosniff": true
}
}
}' | jq .
```
**Gotcha real desta sessão:** se a origem (nginx) já envia o seu próprio
header `Strict-Transport-Security`, **o HSTS do Cloudflare não o sobrepõe**
— é comportamento documentado, não bug. Confirmar com `curl -sI` que o
`cf-cache-status` do pedido é `MISS`/`DYNAMIC` (não é cache stale) e que o
`max-age` devolvido é o da origem, não o configurado no Cloudflare. Ajustar
o `max_age` a partir da origem (nginx) quando o objectivo for realmente
mudar o valor visto pelo browser.
`preload: false` é deliberado por omissão — entrar na lista de preload dos
browsers é quase irreversível (remoção demora meses/anos a propagar). Só
activar depois de HSTS estável em produção há semanas sem incidentes.
O "bom" (`good`) definido no código do plugin para este campo é
`max_age: 31536000` (12 meses), `include_subdomains: 1`, `preload: 1`,
`nosniff: ''` (vazio, i.e. não activa X-Content-Type-Options via este
mecanismo) — mais agressivo do que o valor sugerido na correcção original
desta skill (6 meses). Escolher em função do risco de subdomínios não
cobertos por HTTPS antes de ir para 12 meses + preload.
---
## 4. Certificate Transparency e Leaked Credential Checks
Endpoints próprios, fora do bulk `settings`:
```bash
# Certificate Transparency (alertas quando um certificado é emitido para o domínio)
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
"https://api.cloudflare.com/client/v4/zones/$ZONE/ct/alerting" \
-d '{"enabled": true}' | jq .
# Leaked Credential Checks (avisa se credenciais usadas no site apareceram em leaks conhecidos)
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
"https://api.cloudflare.com/client/v4/zones/$ZONE/leaked-credential-checks" \
-d '{"enabled": true}' | jq .
```
Ambos vinham `off` por omissão num site com anos de conta Cloudflare — não
assumir que "conta antiga = tudo activo por defeito"; a Cloudflare tem
adicionado features de segurança ao longo do tempo sem as retroactivar em
zonas já existentes.
---
## 5. Bot Management (endpoint `/bot_management`) — 7 campos + `is_robots_txt_managed`
Endpoint próprio: `GET`/`PUT /zones/{zone}/bot_management` (não faz parte
do bulk `settings`, e **não aceita `PATCH`**). Estes campos foram
verificados via chamada directa à API (não são geridos pela UI do plugin
através de `getSettingsToManage()`, ao contrário dos campos do §5a) — o
plugin não expõe `content_bots_protection`, `ai_training`, `ai_search`,
`ai_user` nem `is_robots_txt_managed` na sua própria página de Settings,
mas estes são campos reais e válidos do endpoint `bot_management` da
Cloudflare, geríveis por chamada directa à API.
| Campo | Padrão óptimo Descomplicar® | O que faz mal se ligado |
|---|---|---|
| `ai_bots_protection` | `disabled` | Bloqueia crawlers de IA (GPTBot, ClaudeBot, etc.) — visto `block` activo em 2 sites do bundle |
| `content_bots_protection` | `disabled` | Idem, granularidade diferente |
| `crawler_protection` | `disabled` | Bloqueia crawlers genéricos |
| `ai_training` | `disabled` | Bloqueia bots de treino de modelos |
| `ai_search` | `disabled` | Bloqueia bots de motores de busca IA |
| `ai_user` | `disabled` | Bloqueia bots accionados por utilizador de IA (ex. ChatGPT a navegar em nome do utilizador) |
| `fight_mode` | `false` | Desafia (challenge JS) tráfego automatizado por heurística — apanha crawlers legítimos de IA que não resolvem JS challenges, visto a devolver "Just a moment..." em vez de conteúdo real |
**Achado real desta sessão (GEO):** com `is_robots_txt_managed: true`, o
Cloudflare **injecta directivas `Disallow: /`** no `robots.txt` servido
para estes user-agents, por cima do ficheiro físico do site — mesmo que o
ficheiro físico já permita esses bots explicitamente. Parsers que fundem
grupos do mesmo user-agent aplicam o `Disallow` na mesma, tornando a
permissão do ficheiro físico letra morta.
```bash
# Ler estado actual
curl -s -H "Authorization: Bearer $TOKEN" \
"https://api.cloudflare.com/client/v4/zones/$ZONE/bot_management" | jq .
# Corrigir — PUT, NUNCA PATCH (PATCH devolve sempre 10405 Method not allowed,
# independentemente das permissões do token)
curl -s -X PUT -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
"https://api.cloudflare.com/client/v4/zones/$ZONE/bot_management" \
-d '{
"ai_bots_protection": "disabled",
"content_bots_protection": "disabled",
"crawler_protection": "disabled",
"ai_training": "disabled",
"ai_search": "disabled",
"ai_user": "disabled",
"fight_mode": false,
"is_robots_txt_managed": false
}' | jq .
```
Depois de corrigir, **purgar cache** (Cloudflare + cache local WP, ex. WP
Fastest Cache) — `robots.txt` costuma ter `cache-control:
max-age=315360000`, a correcção não aparece sem purge. Confirmar ao vivo
com `curl -sI` que `cf-cache-status` não é `HIT` e o corpo já não tem
`Disallow: /` para os bots visados.
### 5a. Super Bot Fight Mode — campos geridos pela própria UI do plugin
Ao contrário do §5 (chamada directa, campos fora da UI), estes 8 campos
**são** geridos por `getSettingsToManage()` e aparecem na tab "Security"
da página Settings do plugin. Todos escrevem no mesmo endpoint `PUT
/bot_management`, mas com nomes de campo JSON diferentes do que o `id` da
definição no plugin:
| ID no plugin | Campo real na API `bot_management` | Bom (`good`) |
|---|---|---|
| `bot_fight_mode` | `fight_mode` | `false` |
| `ai_bots_protection` | `ai_bots_protection` | `block` *(nota: o `good` do código é `block`, oposto do padrão Descomplicar do §5 — ver decisão abaixo)* |
| `crawler_protection` | `crawler_protection` | `enabled` *(idem — oposto do padrão Descomplicar)* |
| `bot_likely_automated` | `sbfm_likely_automated` (select `allow`/`block`/`managed_challenge`) | `allow` |
| `bot_definitely_automated` | `sbfm_definitely_automated` (select `allow`/`block`/`managed_challenge`) | `allow` |
| `bot_verified_bots` | `sbfm_verified_bots` (select `allow`/`block`) | `allow` |
| `bot_static_resource_protection` | `sbfm_static_resource_protection` (bool) | `false` |
| `bot_optimize_wordpress` | `optimize_wordpress` (bool) | `false` |
| `bot_enable_js` | `enable_js` (bool) — comentário no código: "a API diz que é editável, mas só é possível de facto em plano Pro ou superior" | `false` |
**Contradição intencional a documentar:** o `good` que o código do plugin
define para `ai_bots_protection`/`crawler_protection` é o valor "block"/
"enabled" (i.e. o próprio autor do plugin recomenda bloquear bots de IA
por omissão). A decisão Descomplicar® (documentada no §5 e no gotcha da
tabela §7) é o oposto — **nunca bloquear bots de IA em nenhum site do
bundle**, por ser GEO-crítico (visibilidade em AI Overviews/ChatGPT/
Perplexity). Ao corrigir zonas, ignorar deliberadamente o "good" do plugin
para estes 2 campos específicos e aplicar sempre `disabled`.
Todas as escritas neste grupo enviam `unset_on_write: ['using_latest_model']`
— o código remove explicitamente esse campo do payload antes de fazer
`PUT`, porque `using_latest_model` é campo só de leitura devolvido pela API
(indica se a Cloudflare está a usar o modelo de detecção de bots mais
recente) e a API rejeita o `PUT` se ele for reenviado.
---
## 6. Scopes de token — lista completa (21 permissões, do código de setup)
`Admin/Template/MultisiteSettings.php` e `Admin/Template/Settings.php`
embutem a lista completa e oficial de scopes recomendados pelo autor do
plugin (usada para construir o link de criação de token pré-preenchido no
dashboard Cloudflare) — muito mais granular do que os 6 grupos informais
documentados na versão anterior desta skill:
| Scope Cloudflare | Nível de acesso | Necessário para |
|---|---|---|
| `Account.Access: Apps and Policies` | Edit | Zero Trust Access (página Access, Pro) |
| `Account.Access: Organizations, Identity Providers, and Groups` | Read | Zero Trust Access — resolver grupos/IdPs nas políticas |
| `Account.Account Analytics` | Read | Dados agregados de conta (não usado nesta skill) |
| `Account.Account Settings` | Read + Edit | Detalhes/definições de conta Cloudflare |
| `Account.Allow Request Tracer` | Read | Página "HTTP request trace" (§10) |
| `Account.Billing` | Read | Info de plano/facturação Cloudflare mostrada no plugin |
| `Account.Intel` | Read | Páginas "IP address details"/"Domain details"/"WHOIS" (Cloudflare Intel API, §10) |
| `Account.Turnstile` | Edit | Criar/gerir sitekeys Turnstile CAPTCHA (§8, `cfTurnstile`) |
| `Account.Workers R2 Storage` | Edit | R2 media storage (página R2, Pro) |
| `Account.Workers Scripts` | Edit | Worker proxy (`cfWorkersSubdomain`/`cfProxy`, §8) |
| `User.API Tokens` | Read | O plugin lê as próprias permissões do token para colorir a checklist de setup a verde |
| `Zone.Analytics` | Read | Web Analytics / dashboards de tráfego |
| `Zone.Bot Management` | Edit | Bot Management + Super Bot Fight Mode (§5, §5a) — **separado** do Zone Settings, falta frequentemente no token gerado por omissão |
| `Zone.Cache Purge` | Purge | Purge de cache (página "Purge cache", botão admin bar) |
| `Zone.Cache Rules` | Edit | Cache Rules (página Rules, Pro) |
| `Zone.Firewall Services` | Edit | Firewall Rules, regras de User-Agent/IP (página Firewall, Pro) |
| `Zone.Page Rules` | Edit | Page Rules legadas (página Rules, Pro) |
| `Zone.SSL and Certificates` | Edit | Certificate Transparency, TLS client auth |
| `Zone.Zone` | Edit | Operações de gestão da própria zona |
| `Zone.Zone Settings` | Edit | As ~52 definições de zona do §1 (SSL/TLS, Speed, Caching, Network, Scrape Shield) |
| `Zone.Zone WAF` | Edit | WAF/Managed Ruleset (§2) |
`Zone.DNS` **não está na lista oficial do plugin** — o plugin não gere DNS
(DMARC/SPF/DKIM são lidos como registos públicos via `dig`, ver §7/§10, não
via API Cloudflare autenticada).
**Sintoma de scope em falta:** a API devolve `403` ou o próprio plugin
mostra um erro de permissão específico à operação, não um erro genérico de
autenticação — o token pode estar válido e ainda assim faltar-lhe
exactamente o scope daquela chamada. Testado nesta sessão: token com `Zone
SSL/Settings Edit` funcionava para as definições de zona mas falhava em
Bot Management até se alargar explicitamente o scope no dashboard
Cloudflare (`My Profile → API Tokens → editar token`).
---
## 7. Gotchas e erros comuns
| Sintoma | Causa | Solução |
|---|---|---|
| `PATCH /zones/{id}/settings/waf` → `1027 WAF is deprecated for this zone` | Toggle legado morto para zonas novas | Implantar Managed Ruleset na fase `http_request_firewall_managed` (§2) |
| `PATCH /zones/{id}/bot_management` → `10405 Method not allowed` | Endpoint só aceita `PUT` | Usar `PUT` sempre, mesmo para alterar um único campo (envia o objecto completo) |
| Correcção de `robots.txt`/HSTS não aparece no browser | Header/ficheiro servido a partir de cache (Cloudflare e/ou plugin de cache WP) | Purgar Cloudflare + cache WP local; confirmar `cf-cache-status` não é `HIT` antes de validar |
| HSTS configurado no Cloudflare mas browser continua a ver `max-age` diferente | Origem (nginx) já envia o seu próprio header `Strict-Transport-Security` — Cloudflare **não sobrepõe** HSTS que a origem já envia | Ajustar na origem, ou aceitar que o Cloudflare é só fallback para origens sem HSTS próprio |
| Regra de firewall/Transform Rule com operador `matches` (regex) falha ou não fica disponível na UI | `matches` exige plano **Business** (WAF Advanced) | Reescrever a expressão com `in`, `eq` ou `contains` — disponíveis em qualquer plano, incluindo Free |
| `wp option update app_for_cf '...'` apaga sub-chaves que não estavam no valor enviado | `wp option update` substitui a opção inteira; `app_for_cf` é um array serializado | Usar `wp option patch update app_for_cf <chave> <valor>` para editar uma sub-chave sem tocar nas restantes |
| R2 (media storage) devolve `10042 Please enable R2 through the Cloudflare Dashboard` | R2 não está activado a nível de **conta** (não é definição de zona nem do plugin) | Activar manualmente no dashboard Cloudflare da conta antes de qualquer chamada à API de R2 |
| `ai_bots_protection`/`crawler_protection` a `block`/`enabled` num site do bundle | Divergência do padrão óptimo — visto em `carstuff.pt` e `solarfvengenharia.com` nesta sessão; **o próprio `good` do código do plugin recomenda `block`** (§5a), por isso corrigir manualmente sempre que se aplicar EasyConfig ou se seguir uma recomendação genérica do plugin | Corrigir para `disabled` via `PUT /bot_management` (§5) — decisão explícita: nunca bloquear bots de IA em nenhum site Descomplicar |
| Página "Public page caching"/"Purge cache"/analytics parecem não fazer nada de imediato | `handleHeaders()` só define `Cache-Control: max-age=0,s-maxage=X` — depende do Cloudflare respeitar esse header (`cache_level`/`browser_cache_ttl` compatíveis) e de não haver cookies de sessão a bloquear o cache (ver §9) | Confirmar `browser_cache_ttl` = "Respect existing headers" (`0`) e `cache_level` ≠ `basic`; testar em aba anónima sem cookies `wp-*`/`wordpress_*`/`comment_*`/`woocommerce_*` |
| Botões "Firewall", "Access", "Rules" (criar regra), "R2", "Copy from...", "License key" aparecem desactivados/a cinzento ou com classe `pro` | Funcionalidade gated para a versão **Premium/Pro** do plugin (`Helper\Api::check()` — ver §11) | Confirmar licença activa (`cfLicenseKey`) ou aceitar a limitação no plano Free |
| `wp option get app_for_cf` mostra `cfTurnstile:["0"]` em vez do array completo com `siteKey`/`onLogin`/etc. | Turnstile nunca foi activado neste site — o checkbox "Use Turnstile CAPTCHA" desmarcado grava só `[0]` (o `<input type="hidden" value="0">` do formulário), não o array completo de `Setup::defaults()` | Normal em sites sem Turnstile configurado; a estrutura completa (13 sub-chaves, ver §8) só aparece depois de gerar uma sitekey e gravar o formulário |
---
## 8. Config do plugin (`wp_options.app_for_cf`) — as 18 chaves em detalhe
Fonte: `Setup::defaults()` (schema oficial de instalação) cruzado com a
leitura ao vivo (`wp option get app_for_cf`) em `emanuelalmeida.pt`
(16-08-2026) — as 18 chaves batem certo entre defaults e produção.
```json
{"cfWorkersSubdomain":"","cfProxy":{"image":"0","url":"0"},"cfR2Bucket":{"media":""},"cfAccountId":"a8b00ad3227d729a146143a6a85a7522","cfZoneId":"574b243ebabd8ea2b8d89e68921f52cc","cfTokenId":"6b6d24fe8d324112ea4cfc328bf16f6c","cfZone":"emanuelalmeida.pt","cfPageCachingSeconds":"21600","cloudflareAuth":{"token":"cfut_..."},"cloudflareFirewallExpireDays":"7","cloudflareBlockIpsSpamClean":"0","cfExternalDataUrl":"","cloudflarePreload":"1","cfImagesTransform":"0","cfLicenseKey":"","cfPurgeCacheOnAdminBar":"1","cfTurnstile":["0"],"LockSettingsUserId":"1"}
```
| Chave | Tipo/Default | O que faz |
|---|---|---|
| `cfWorkersSubdomain` | string, `''` | Subdomínio `*.workers.dev` da conta Cloudflare, usado quando o Worker proxy (R2/imagens) está activo |
| `cfProxy.image` / `cfProxy.url` | `0`/`1` cada | Toggle interno para saber se URLs de imagem/media já estão a ser reescritas para servir via Worker/R2 (usado por `filterWpGetAttachmentUrl()`/`filterWpCalculateImageSrcset()` em `Base/Pub.php`) |
| `cfR2Bucket.media` | string, `''` | Nome do bucket R2 escolhido/criado para armazenar media da biblioteca WordPress (feature Pro, página R2) |
| `cfAccountId` | string | ID da conta Cloudflare — resolvido automaticamente por `getZoneId()` a partir do hostname na primeira chamada, cacheado aqui |
| `cfZoneId` | string | ID da zona Cloudflare — idem, cacheado para evitar `listZones()` a cada pedido |
| `cfTokenId` | string | ID do token API (não o valor secreto) — mostrado na UI como referência ("Token ID: ...") para o admin confirmar qual token está activo |
| `cfZone` | string | Nome do domínio da zona (ex. `emanuelalmeida.pt`), cacheado junto com `cfAccountId`/`cfZoneId` |
| `cfPageCachingSeconds` | int (segundos), `''` (vazio = desligado) | Tempo de `s-maxage` aplicado ao HTML para guests (§9) — `0`/vazio desliga o guest page caching |
| `cloudflareAuth.type` | `'token'` ou `'email'` | Modo de autenticação — `token` é o único usado/recomendado; `email`+`api_key` é o modo legado Global API Key |
| `cloudflareAuth.token` | string secreta | Bearer token da Cloudflare API, scoped (ver permissões no §6) |
| `cloudflareAuth.email` / `cloudflareAuth.api_key` | string, `''` | Só usados se `type = 'email'` (não usar) |
| `cloudflareFirewallExpireDays` | int, default `7` (1-90) | Dias até uma regra de firewall **criada automaticamente pelo plugin** (ex. bloqueio de IP de spammer, feature Pro) expirar sozinha |
| `cloudflareBlockIpsSpamClean` | `0`/`1`, default `1` | Se `1`, bloqueia automaticamente (feature Pro) o IP de um utilizador cujo comentário foi marcado como spam pela fila de moderação do WordPress |
| `cfExternalDataUrl` | string URL, `''` | URL pública onde o media fica acessível quando R2 está activo — normalmente auto-preenchido ao activar R2, não editar manualmente salvo troubleshooting |
| `cloudflarePreload` | `0`/`1`, default `1` | Se `1`, o plugin usa hooks `script_loader_tag`/`style_loader_tag`/`wp_footer` para emitir headers HTTP `Link: <url>;rel=preload` (até 10 recursos) — funciona em conjunto com a definição de zona `early_hints` |
| `cfImagesTransform` | `0`/`1`, default `0` | Se `1`, activa **Cloudflare Images Transform** para blocos de imagem/media (serve AVIF/WebP a browsers modernos, formatos mais antigos a browsers antigos, qualidade reduzida em ligações muito lentas) — requer activação também a nível de zona no dashboard Cloudflare |
| `cfLicenseKey` | string, `''` | Chave de licença da versão Premium/Pro do plugin — sem isto, `Helper\Api::check()` devolve sempre `false` (ver §11) |
| `cfPurgeCacheOnAdminBar` | `0`/`1`, default `0` | Se `1`, mostra um botão de purge de cache com um clique na admin bar do WordPress |
| `cfTurnstile` | array com 13 sub-chaves quando activo: `siteKey`, `secretKey`, `onRegister`, `onLogin`, `onPassword`, `onComment`, `onContactForm7`, `onHtmlForms`, `onMetForm`, `onWPForms`, `onWooCommerceRegister`, `onWooCommerceLogin`, `onWooCommercePassword` | CAPTCHA Turnstile aplicado por contexto — cada `onX` é um toggle independente que decide se o widget Turnstile aparece nesse formulário específico (registo/login/reset password/comentários WP nativos, WooCommerce, e os plugins de formulário Contact Form 7/HTML Forms/MetForm/WPForms se estiverem instalados) |
| `LockSettingsUserId` | `0` ou ID de utilizador WP | Se preenchido com um ID de utilizador, **só essa conta WordPress** pode alterar as definições Cloudflare a partir do wp-admin — risco: apagar essa conta ou trocar de utilizador tranca o acesso às definições |
`network_exclude` (visto em código de multisite, `Settings.php` linha 290)
é uma chave adicional só relevante em instalações multisite — indica que
um site individual optou explicitamente por **não** herdar o token
API de rede.
---
## 9. Guest page caching (HTML a nível de edge) — como funciona de facto
Página "Public page caching" (§10) e a chave `cfPageCachingSeconds` (§8)
não usam Page Rules nem Cache Rules Cloudflare — usam um mecanismo mais
simples e mais portável: o hook `wp_headers` do WordPress
(`Base\Pub::handleHeaders()`, `Base/Pub.php` linha 276) define
```
Cache-Control: max-age=0,s-maxage=<cfPageCachingSeconds>
```
em toda resposta HTML elegível. `max-age=0` diz ao browser para não
cachear localmente; `s-maxage=<N>` diz a caches partilhadas (o edge da
Cloudflare) para cachear durante N segundos — desde que a definição de
zona `browser_cache_ttl` esteja em `0` ("Respect Existing Headers", o
padrão recomendado no §1) e `cache_level` não seja `basic`.
**Critérios de elegibilidade** (`pageCachingCriteria()`, filtráveis via
`app_for_cf_guest_page_cacheable`):
- Utilizador **não** tem sessão WordPress activa (`is_user_logged_in()`
falso)
- Nenhum cookie do browser começa por `wp-`, `wordpress_`, `comment_`
ou `woocommerce_` (senão a página é tratada como dinâmica/pessoal e o
header de cache não é aplicado)
- Pedido não é para `/wp-login.php`
- Content-Type da resposta começa por `text/html`
- Página não é 404
- `WP_DEBUG` não está activo
Por omissão o valor sugerido na UI é 6 horas (`21600` segundos, a mesma
opção `default` usada em `CachingGuestPage.php`); opções disponíveis vão
de 5 minutos a 24 horas, filtráveis via `app_for_cf_cache_times`, e o
limite máximo é derivado da vida do nonce do WordPress
(`DAY_IN_SECONDS - 3600`, tipicamente 23h).
```bash
# Ler/alterar o tempo de guest page caching
wp option pluck app_for_cf cfPageCachingSeconds --allow-root --path=$PATH
wp option patch update app_for_cf cfPageCachingSeconds 21600 --allow-root --path=$PATH # 6h
wp option patch update app_for_cf cfPageCachingSeconds '' --allow-root --path=$PATH # desligar
```
Como não é uma Page/Cache Rule, **não aparece no dashboard Cloudflare**
como regra — só é visível inspeccionando o header `Cache-Control` da
resposta HTTP (`curl -sI https://SITE/`) e o `cf-cache-status`.
---
## 10. As 14 páginas de admin — o que cada uma faz
Estrutura de menu confirmada em `Admin/Base/Admin.php`. O item de topo
(`Cloudflare`, ícone `dashicons-cloud`) mostra um badge `!` quando não há
token configurado.
| Slug (`page=`) | Título no menu | O que faz |
|---|---|---|
| `app-for-cf_caching` | **Public page caching** | Toggle + selector de tempo para o guest page caching do §9 (`cfPageCachingSeconds`) |
| `app-for-cf_firewall` | **Firewall** | 3 tabelas: **Firewall Rules** (link "Create rule" abre o dashboard Cloudflare — Pro), **User agents** (regras de bloqueio por user-agent geridas inline — Pro) e **IP addresses** (bloqueio manual por IP — Pro); todas suportam bulk enable/disable/delete via `bulkActionNotice()` |
| `app-for-cf_access` | **Access** | Lista de Zero Trust Access Apps + Groups da conta, com delete em massa (feature **Pro** — cria políticas de acesso, ex. proteger `/wp-admin` com SSO) |
| `app-for-cf_rules` | **Rules** | 2 tabelas: **Page Rules** legadas (link "Create page rule" abre dashboard — Pro) e **Cache Rules** modernas (link "Create cache rule" — Pro) |
| `app-for-cf_r2` | **R2 (media storage)** | Explica e liga a biblioteca de media do WordPress a um bucket Cloudflare R2 (10GB grátis, depois $0.015/GB); requer token válido **e** licença Pro |
| `app-for-cf_cache` | **Purge cache** | Botão único "Purge Cloudflare cache..." — chama `purgeCache()` sem filtro (`Purge Everything`) |
| `options-general.php?page=app-for-cf` | **Settings** (link para `wp-admin/options-general.php?page=app-for-cf`) | A página principal com as 6 tabs de definições de zona (Setup/SSL-TLS/Security/Speed/Caching/Network+Scrape Shield) — ver §1; badge "Missing API token" se `cloudflareAuth.token` vazio |
| `app-for-cf_analytics` | **Web analytics** | Toggle único para injectar o beacon JavaScript do Cloudflare Web Analytics (privacy-first, sem cookies) em todas as páginas; link directo para o dashboard de analytics quando activo |
| `app-for-cf_dmarc` | **DMARC management** | Gráfico (Chart.js) de volume de email pass/fail por semana/mês + tabela de fontes de envio detectadas via os relatórios DMARC que a Cloudflare recebe em nome do domínio; link "View in Cloudflare" para o dashboard |
| `app-for-cf_request-trace` | **HTTP request trace** | Formulário para simular um pedido (URL, método, protocolo, bot score, país, threat score, skip challenge) e ver que produtos/regras Cloudflare seriam aplicados — requer scope `Account.Allow Request Tracer` |
| `app-for-cf_ip-details` | **IP address details** | Consulta a Cloudflare Intel API para um IP: PTR, ASN/organização, tipo, risk types — requer `Account.Intel:Read` |
| `app-for-cf_domain-details` | **Domain details** | Idem para domínios: tipo, IPs de resolução, categorias de conteúdo |
| `app-for-cf_whois` | **WHOIS** | Consulta WHOIS via Cloudflare Intel: datas de criação/actualização, registrant, registrar, nameservers |
| (sem entrada de menu própria) **Copy Settings** | acedida a partir de um botão na página Settings | Copia todas as definições de zona de uma zona diferente da mesma conta Cloudflare para a zona actual — **irreversível**, exige confirmação explícita ("I understand...") antes de activar o botão de submissão |
Páginas exclusivas de **multisite network admin** (`settings.php?page=`):
| Slug | Título | O que faz |
|---|---|---|
| `app-for-cf_multisite-settings` | **Cloudflare** (em Network Admin → Settings) | Define um token API partilhado por toda a rede (`app_for_cf_network`), com a mesma checklist de 21 permissões do §6; cada site individual pode optar por usar o token de rede ou o seu próprio |
| `app-for-cf_multisite-r2` | **R2 (media storage)** (Network Admin) | Um único bucket R2 partilhado por todos os sites da rede, com o caminho de cada ficheiro prefixado por `{siteId}/` (ex. `cdn.site.com/50/2023/10/image.png`) — feature Pro, licenciada ao hostname do site principal da rede |
---
## 11. Free vs Pro — gate `Helper\Api::check()`
`Helper\Api::$version` / `Helper\Api::check($force = false)` (`Helper/Api.php`)
determina se a licença Premium está activa através de um **transient**
(`acf_int`) com 6 horas de validade (`21600` segundos) — não é lido de
`cfLicenseKey` directamente a cada pedido, é cacheado. `Setup::install()`
chama `Api::check(true)` (forçado) na activação do plugin.
Funcionalidades bloqueadas/degradadas no plano Free (confirmado nos
templates admin, condicional `Helper\Api::$version` ou classe CSS `pro`):
- Botão "Create rule"/"Create page rule"/"Create cache rule"/"Create user
agent rule"/"Create IP address rule" (Firewall, Rules) — sem eles, as
tabelas continuam a **listar** regras já criadas manualmente no
dashboard Cloudflare, só não permitem criar a partir do wp-admin
- Página R2 (media storage) — bloqueada mesmo com token válido
- Página Access (Zero Trust) — lista mas não gere
- "Copy from..." (Copy Settings) — botão só aparece se
`Helper\WordPress::hasOwnApiToken()` **e** implicitamente exige Pro
para ser útil em produção
- Campo "License key" só aparece se `Helper\Api::$version` já tiver
alguma licença associada (mostra o campo para trocar/renovar)
- "Block spammer IPs"/"Days that firewall rules last"/"External data
URL" (`cloudflareBlockIpsSpamClean`, `cloudflareFirewallExpireDays`,
`cfExternalDataUrl`) aparecem com classe CSS `pro` e `disabled` quando
`(int)$cloudflareAppInternal` é falso — os campos ficam visíveis mas
não editáveis
Todas as definições de zona do §1 (SSL/TLS, Speed, Caching, Network,
Scrape Shield, Bot Management/SBFM), o WAF via Managed Ruleset (§2), HSTS
(§3), Certificate Transparency/Leaked Credential Checks (§4), guest page
caching (§9), purge de cache e Turnstile CAPTCHA (§8) **funcionam no plano
Free do plugin** — não são gated, só dependem do plano Cloudflare
subjacente (Free Website vs Pro/Business), que é uma dimensão diferente.
---
## 12. Automação — WP-CLI e Cron
```bash
# Purgar cache Cloudflare (Purge Everything) via linha de comandos
wp app-for-cf purge-cache --allow-root --path=$PATH
```
Implementado em `Cli/PurgeCache.php` (comando `app-for-cf purge-cache`,
`@when after_wp_load`) — chama `Repository\Cloudflare::purgeCache()` sem
argumentos (equivalente ao botão da página "Purge cache", §10).
`Cron\Jobs::purgeCache(array $urls)` é o mecanismo interno usado por
`Base\Pub::purgeCacheByPostIds()` quando um post muda de estado
(publicado/editado) ou um comentário muda de estado — faz **chunking em
lotes de 30 URLs** por chamada à API de purge (limite da Cloudflare por
pedido), parando o loop no primeiro chunk que falhar em vez de continuar a
tentar os restantes.
---
## 13. Áreas do plugin sem config a auditar/activar (confirmado nesta sessão, `emanuelalmeida.pt`)
| Área | Estado |
|---|---|
| Cache Rules / Page Rules | 0 regras — `GET /zones/{id}/pagerules` → `count:0` |
| Access (Zero Trust) | Não configurado, fora de âmbito nesta sessão — funcionalidade real do plugin, ver §10 |
| Turnstile CAPTCHA | Site key existe mas desligada no plugin (`cfTurnstile: ["0"]`) — infra pronta se decidido substituir reCAPTCHA; ver estrutura completa das 13 sub-chaves no §8 |
| Workers proxy (`cfProxy`) | `{image: off, url: off}` |
| R2 (media storage) | `cfR2Bucket.media` vazio, `cfExternalDataUrl` vazio — nunca activado neste site; requer licença Pro (§11) |
| DMARC/SPF/DKIM (`Dmarc.php`) | **Auditar via DNS público directo** (`dig TXT`), nunca precisa de token Cloudflare — são registos públicos por desenho; a página DMARC do plugin (§10) é sobre os relatórios agregados que a Cloudflare recebe, não sobre editar os próprios registos DNS |
| Web Analytics (beacon JS) | Confirmar por ausência/presença de `cloudflareinsights`/`beacon.min.js` no HTML servido, não por option — a página Web Analytics (§10) tem toggle próprio fora de `wp_options` (via API, não confirmado nesta sessão se lê de zona ou de conta) |
| EasyConfig (`setEasyMode()`) | Aplica tudo em bloco (lista exacta no §1) — preferir aplicar definição a definição para manter controlo e rasto de decisão |
| Licença Pro (`cfLicenseKey`) | Vazia — plugin a correr em modo Free; ver §11 para o que fica indisponível |
---
## Fonte
`CONFIG-Plugins-Referencia.md` §1 (App for Cloudflare®) — achados originais
de HSTS/WAF/DMARC, credenciais de sessão. `BUNDLE-Excelencia-WP.md` §2.2
(Performance) e §2.4 (GEO — bots de IA, achado completo do
`is_robots_txt_managed` e dos 7 campos de Bot Management via API directa).
Ambos em `Hub/04-Stack/02.04-Sistemas/71.Seguranca/`, piloto
`emanuelalmeida.pt`, sessão 16-08-2026.
**Expansão desta sessão (16-08-2026):** leitura completa do código-fonte
do plugin `app-for-cf` v1.10.0 via SSH (`server`, 61 ficheiros PHP,
11.309 linhas) — não apenas os ficheiros tocados na auditoria pontual
original. Ficheiros lidos na íntegra: `app-for-cf.php` (bootstrap),
`Setup.php` (schema de `wp_options`), `Repository/Cloudflare.php` (2178
linhas — lista completa de 52 definições de zona + `setEasyMode()` +
lógica de leitura/escrita), `Admin/Template/Settings.php` (766 linhas —
UI completa, scopes de token, Turnstile), `Admin/Base/Admin.php` (menu,
14 páginas), `Helper/Api.php` (gate Free/Pro), `Cron/Jobs.php`,
`Cli/PurgeCache.php`, e os 14 templates individuais em `Admin/Template/`
(`Firewall.php`, `R2.php`, `Access.php`, `Rules.php`, `Analytics.php`,
`Dmarc.php`, `RequestTrace.php`, `IpDetails.php`, `DomainDetails.php`,
`Whois.php`, `CachingGuestPage.php`, `Caching.php`, `Cache.php`,
`CopySettings.php`, `MultisiteR2.php`, `MultisiteSettings.php`) e trechos
relevantes de `Base/Pub.php` (657 linhas — guest page caching, preload,
purge). Confirmado ao vivo com `wp option get app_for_cf --format=json`
em `emanuelalmeida.pt` que as 18 chaves de `wp_options` batem certo com
`Setup::defaults()`. Nenhuma escrita feita em produção durante esta
expansão — só leitura de código-fonte e `wp option get`.
@@ -0,0 +1,70 @@
---
name: elementor-pro-widgets
description: Catálogo completo e verificado dos widgets nativos Elementor Pro (30 gerais + 5 WooCommerce) e como os descobrir/adicionar via EMCP Tools. Usar quando "widgets elementor pro", "que widgets pro existem", "loop grid", "nested tabs", "form elementor pro", "table of contents elementor", "portfolio elementor", "reviews carousel elementor".
layer: wiki
---
# /elementor-pro-widgets — Catálogo de Widgets Nativos Elementor Pro
Catálogo obtido ao vivo via `list-widgets(tier:"pro")` num site Elementor Pro 4.0.0 (EMCP Tools), cruzado com a documentação oficial. Widget types testados como legacy (`add-free-widget`/`add-pro-widget` + `widget_type`), não atómicos — Elementor Pro ainda não migrou o catálogo completo para elementos atómicos 4.0.
## Como descobrir e adicionar (workflow)
1. `list-widgets({ tier: "pro" })` — confirma quais dos 30+5 abaixo estão realmente disponíveis nesta ligação (Elementor Pro tem de estar activo).
2. `get-widget-schema({ widget_type })` — parâmetros curados (obrigatórios, defaults, enums). **Campo é `widget_type`, não `type`.**
3. `add-pro-widget({ post_id, parent_id, widget_type, settings })` num container legacy (`add-container`), ou `add-free-widget` se a tool `add-pro-widget` estiver desligada nesse site (verificar com `list-widgets` primeiro — `tier:"pro"` a devolver resultados não implica que `add-pro-widget` esteja montada; é frequentemente a primeira tool fechada num deny-list de produção).
4. Ver skill `emcp-page-building` para as regras de coexistência atómico/legacy e para `batch-update`/`update-element` genérico.
## Catálogo — widgets gerais (30)
| `widget_type` | Título | Uso |
|---|---|---|
| `form` | Form | Formulários de contacto/captação com tipos de campo configuráveis, acções de submissão |
| `posts` | Posts Grid | Grelha pesquisável de posts/pages com contagem de colunas e paginação |
| `countdown` | Countdown | Temporizador com modo data-limite ou evergreen, labels e mensagem de expiração custom |
| `price-table` | Price Table | Tabela de plano de preços com moeda, preço promocional, lista de features, ribbon, CTA |
| `flip-box` | Flip Box | Cartão de dois lados que vira ao hover, cada lado com gráfico/título/descrição |
| `animated-headline` | Animated Headline | Título com efeito de destaque ou texto rotativo |
| `call-to-action` | Call to Action | Bloco CTA com título, descrição, botão e gráfico/ribbon opcional |
| `slides` | Slides | Slider hero full-width com heading/descrição/botão/background por slide |
| `testimonial-carousel` | Testimonial Carousel | Carrossel rotativo de testemunhos com skins, layouts e navegação |
| `price-list` | Price List | Lista tipo menu com título, preço, descrição, imagem e separador |
| `gallery` | Gallery | Galeria de imagens avançada com layouts grid/justified/masonry e filtros |
| `share-buttons` | Share Buttons | Botões de partilha social para a página actual com skins e formas |
| `table-of-contents` | Table of Contents | Índice auto-gerado a partir dos headings da página, com marcação de secção activa |
| `blockquote` | Blockquote | Citação estilizada com texto, atribuição de autor, opção de tweet |
| `lottie` | Lottie Animation | Embed de animação Lottie JSON com triggers, loop, velocidade, renderer |
| `hotspot` | Hotspot | Imagem interactiva com pontos clicáveis/hover configuráveis |
| `nav-menu` | Navigation Menu | Menu de navegação WordPress com layouts, ponteiros de hover, breakpoint de dropdown |
| `loop-grid` | Loop Grid | Posts/pages/CPTs em grelha renderizada por um loop template |
| `loop-carousel` | Loop Carousel | Posts em carrossel renderizado por um loop template, responsivo |
| `media-carousel` | Media Carousel | Carrossel de imagens/vídeos com skins carousel/slideshow/coverflow |
| `nested-tabs` | Nested Tabs | Tabs modernas onde cada conteúdo de tab é um container populável livremente |
| `nested-accordion` | Nested Accordion | Acordeão moderno onde cada item é um container populável livremente |
| `portfolio` | Portfolio | Grelha filtrável de posts/CPTs para portfólio/trabalho, com modal |
| `author-box` | Author Box | Avatar, nome, bio do autor do post e link para os posts dele |
| `login` | Login | Formulário de login frontend com labels, remember-me, link de password perdida |
| `code-highlight` | Code Highlight | Bloco de código com syntax highlight, linguagem, tema, números de linha |
| `reviews` | Reviews | Carrossel de reviews/testemunhos com slides por vista, autoplay, loop |
| `off-canvas` | Off-Canvas | Painel off-canvas deslizante com posição, largura, animação de entrada |
| `progress-tracker` | Progress Tracker | Indicador de progresso de scroll (horizontal ou circular) |
| `search` | Search | Input de pesquisa do site com dropdown de resultados ao vivo opcional e tabs por post-type |
## Catálogo — widgets WooCommerce (5)
| `widget_type` | Título | Uso |
|---|---|---|
| `woocommerce-products` | WooCommerce Products | Grelha de produtos WooCommerce com colunas/linhas, ordenação |
| `wc-add-to-cart` | WooCommerce Add to Cart | Botão de adicionar ao carrinho para um produto específico, com quantidade |
| `woocommerce-cart` | WooCommerce Cart | Renderiza a página completa do carrinho WooCommerce |
| `woocommerce-checkout-page` | WooCommerce Checkout | Renderiza a página completa de checkout WooCommerce |
| `woocommerce-menu-cart` | WooCommerce Menu Cart | Ícone de mini-carrinho para o menu/header com indicador de itens |
## Notas
- Este catálogo é o que `list-widgets(tier:"pro")` devolveu ao vivo — pode variar ligeiramente por versão do Elementor Pro; correr sempre `list-widgets` primeiro no site alvo em vez de confiar cegamente nesta tabela para um `widget_type` exacto.
- `add-pro-widget` está frequentemente no deny-list de sites de produção (ver skill `emcp-tools` para o mecanismo). Se estiver desligada, reportar ao utilizador em vez de tentar contornar via `add-free-widget` com um `widget_type` Pro — normalmente falha da mesma forma (widget não registado para essa chamada).
## Skills relacionadas
- `emcp-page-building` — sequência de construção de página, coexistência atómico/legacy, `batch-update`.
- `elementskit-widgets` / `powerpack-widgets` — catálogos de addons de terceiros (não aparecem em `list-widgets`, exigem `get-widget-schema(..., full:true)`).
@@ -0,0 +1,73 @@
---
name: elementskit-widgets
description: Catálogo completo e verificado dos 42 widgets ElementsKit Lite (Wpmet) para Elementor, com o widgetType real de cada um extraído do código-fonte — não estão no catálogo curado do EMCP Tools (list-widgets), por isso precisam de get-widget-schema com full:true. Usar quando "elementskit", "elements kit", "widget ekit", "elementskit-button", "que widgets elementskit existem", "nav menu elementskit".
layer: wiki
---
# /elementskit-widgets — Catálogo de Widgets ElementsKit Lite
ElementsKit Lite (Wpmet, plugin `elementskit-lite`) regista os próprios widgets Elementor **fora** do catálogo curado do EMCP Tools — `list-widgets` não os lista. Este catálogo foi extraído directamente do código-fonte (`wp-content/plugins/elementskit-lite/widgets/*/`, ficheiros `*-handler.php`, método `get_name()`) num site em produção com ElementsKit Lite 4.0.1, não adivinhado por convenção de nomes.
## Como usar (workflow)
1. Confirmar que `elementskit-lite` está activo: `list-plugins` e procurar o slug — **atenção:** o site pode ter também `elementskit` (versão Pro) instalado mas **inactivo**; confirmar qual dos dois está `active:true` antes de assumir que widgets Pro do ElementsKit existem.
2. `get-widget-schema({ widget_type: "elementskit-<slug>", full: true })` — estes widgets **não estão no catálogo curado**, por isso a chamada sem `full:true` devolve `{"error": "Not in the curated catalog. Retry with full:true..."}`. `full:true` devolve o schema de controlos bruto e funciona.
3. Adicionar com `add-free-widget({ post_id, parent_id, widget_type, settings })` num container **legacy** (`add-container`) — confirmado ao vivo que widgets ElementsKit são legacy, não têm equivalente atómico. Ver skill `emcp-page-building` para a mecânica de coexistência atómico/legacy.
4. **Nunca assumir o `widgetType` por convenção `elementskit-<pasta>`** — a tabela abaixo tem duas excepções confirmadas (`nav-menu` e `tab`); verificar sempre com `get-widget-schema(full:true)` antes de construir em produção.
## Catálogo (42 widgets, `widgetType` real verificado no código-fonte)
| `widgetType` | Título | Categoria/nota |
|---|---|---|
| `elementskit-accordion` | Accordion | geral |
| `elementskit-back-to-top` | Back to Top | geral |
| `elementskit-blog-posts` | Blog Posts | posts |
| `elementskit-business-hours` | Business Hours | marketing |
| `elementskit-button` | Button | geral — visto ao vivo em uso real |
| `elementskit-category-list` | Category List | geral |
| `elementskit-client-logo` | Client Logo | marketing |
| `elementskit-contact-form7` | Contact Form 7 | integração — exige plugin CF7 activo |
| `elementskit-countdown-timer` | Countdown Timer | marketing |
| `elementskit-drop-caps` | Drop Caps | geral |
| `elementskit-dual-button` | Dual Button | geral |
| `elementskit-faq` | FAQ | geral |
| `elementskit-fluent-forms` | Fluent Forms | integração — exige Fluent Forms activo |
| `elementskit-funfact` | Funfact | criativo |
| `elementskit-header-info` | Header Info | geral |
| `elementskit-header-offcanvas` | Header Offcanvas | geral |
| `elementskit-header-search` | Header Search | geral |
| `elementskit-heading` | Heading | geral |
| `elementskit-icon-box` | Icon Box | geral |
| `elementskit-icon-hover` | Icon Hover | geral |
| `elementskit-image-accordion` | Image Accordion | geral |
| `elementskit-image-box` | Image Box | geral |
| `elementskit-image-comparison` | Image Comparison | geral |
| `elementskit-lottie` | Lottie | animação |
| `elementskit-mail-chimp` | Mail Chimp | integração — exige Mailchimp configurado |
| **`ekit-nav-menu`** | ElementsKit Nav Menu | geral — **excepção: prefixo `ekit-`, não `elementskit-`** |
| `elementskit-ninja-forms` | Ninja Forms | integração — exige Ninja Forms activo |
| `elementskit-page-list` | Page List | geral |
| `elementskit-piechart` | Pie Chart | geral |
| `elementskit-post-grid` | Post Grid | posts |
| `elementskit-post-list` | Post List | posts |
| `elementskit-post-tab` | Post Tab | posts |
| `elementskit-pricing` | Pricing Table | marketing |
| `elementskit-progressbar` | Progress Bar | geral |
| `elementskit-social-media` | Social | geral — nota: pasta chama-se `social`, widgetType é `elementskit-social-media` |
| `elementskit-social-share` | Social Share | geral |
| **`elementskit-simple-tab`** | Tab | geral — **excepção: pasta chama-se `tab`, widgetType é `elementskit-simple-tab`** |
| `elementskit-tablepress` | TablePress | integração — exige plugin TablePress activo |
| `elementskit-team` | Team | geral |
| `elementskit-testimonial` | Testimonial | geral — visto ao vivo em uso real |
| `elementskit-video` | Video | geral |
| `elementskit-we-forms` | WeForms | integração — exige weForms activo |
| `elementskit-wp-forms` | WP Forms | integração — exige WPForms activo, visto ao vivo em uso real |
## Definições transversais injectadas pelo ElementsKit em QUALQUER widget
Confirmado ao vivo em `get-element-settings` de um widget `heading` legacy comum (não um widget ElementsKit): a presença do ElementsKit Lite injecta a definição `ekit_cursor_text_label` ("Elementskit Cursor") em widgets genéricos via uma extensão global de cursor. Não é exclusivo dos 42 widgets acima — pode aparecer em qualquer widget legacy no site.
## Skills relacionadas
- `emcp-page-building` — mecânica de coexistência atómico/legacy, sequência de construção.
- `elementor-pro-widgets` — catálogo nativo Elementor Pro (curado, no `list-widgets`).
- `powerpack-widgets` — catálogo do addon concorrente PowerPack Elements (também fora do catálogo curado).
@@ -0,0 +1,56 @@
---
name: emcp-content-ops
description: Gestão de conteúdo WordPress (posts/pages/CPTs, taxonomias, media, menus, redirects, blocos Gutenberg, pesquisa de conteúdo) via qualquer ligação MCP EMCP Tools — distinto de construção de páginas Elementor (ver emcp-page-building) ou auditoria de segurança/performance (ver emcp-site-audit). Usar quando "gerir conteúdo wordpress via mcp", "emcp posts", "upload media mcp", "menus wordpress mcp", "redirects mcp", "blocos gutenberg mcp".
layer: wiki
---
# /emcp-content-ops — Operações de Conteúdo WordPress via EMCP Tools
Aplica-se a qualquer ligação MCP EMCP Tools. Confirma sempre o site alvo com `core/get-site-info` — nunca assumir pelo nome da ligação.
## Posts / Pages / CPTs
- `list-post-types` primeiro num site desconhecido — os CPTs variam muito (visto ao vivo: `e-landing-page`, `podcast`, `elementor_library`, `elementskit_content`/`elementskit_template` além dos standard `post`/`page`).
- `create-post({ post_type, title, content, status, slug, author, date, parent, terms, meta, featured_image })` escreve `post_content` (HTML clássico ou markup de blocos Gutenberg) — **não** dados Elementor. Para uma página construída em Elementor usar `create-page` (ver `emcp-page-building`); `create-post` explicitamente não toca em dados Elementor.
- `get-post`/`list-posts`/`update-post`/`delete-post`/`set-post-terms`. `is_elementor` em `get-post`/`list-posts` diz qual editor é dono de determinada página — nunca editar o conteúdo de uma página Elementor via `update-post`.
- `delete-post` vai para o Lixo por omissão (reversível); `force:true` para eliminação permanente. Devolve também um `redirect_suggestion` a lembrar de chamar `create-redirect` para o URL agora morto — agir sobre isso em conteúdo publicado, ignorar em rascunhos/conteúdo de teste.
- `set-post-terms({ mode: replace|append|remove, create_missing })` — **`create_missing:true` deixa um termo de taxonomia permanente** se não houver tool de eliminação de termos montada na ligação (comummente o caso). Confirmar nomes de termos antes de usar em conteúdo descartável.
## Media
- `list-media`/`get-media` para a biblioteca existente (pesquisa por título/alt/legenda/descrição) antes de recorrer a fotos de stock — reutilizar o que já existe.
- `upload-media({ filename, data: base64, alt, title, caption, description })` — bytes vindos do **cliente**, não de um URL. `sideload-image(url)` — o **servidor** vai buscar um URL que consegue alcançar. Casos de uso diferentes, não confundir.
- `search-images`/`add-stock-image` (Unsplash/Pexels/Pixabay) exigem uma chave API do provedor configurada em EMCP Tools → Connection nesse site específico — **verificar que está configurada antes de planear um workflow em torno disso**; um site sem nenhuma configurada devolve um erro simples "sem chave API", não é uma falha da tool em si.
- `delete-media` está comummente no deny-list (destrutivo, o WordPress ignora o Lixo para media por omissão). Se não estiver montada para esta ligação, qualquer upload é efectivamente permanente via MCP — obter confirmação explícita antes de fazer upload, e verificar primeiro se está montada.
- `update-media` para correcções de alt text/título/legenda/descrição — seguro, não-destrutivo, boa opção por omissão para passagens de limpeza de acessibilidade/SEO.
## Menus
`menu-read`/`menu-write` são **tools despachadoras** — chamar sem argumento `operation` primeiro para obter a lista de operações de leitura/escrita disponíveis e as formas exactas de argumento para a versão do plugin nessa ligação, depois chamar de novo com `{ operation, arguments }`. Não adivinhar nomes de operação.
## Redirects
`list-redirects`/`find-broken-links` são só-leitura e comummente activas por omissão. `create-redirect`/`update-redirect`/`delete-redirect` estão comummente no deny-list em sites de produção (mutação de routing com impacto directo em SEO) — verificar se estão montadas antes de assumir que se pode agir sobre um achado de link partido; se não estiverem, reportar o achado e deixar o utilizador criar o redirect via wp-admin ou um canal WP-CLI.
`find-broken-links` é só-leitura e seguro de correr a qualquer momento — propõe correcções, não muda nada. Bom primeiro passo antes de qualquer trabalho de redirects.
## Blocos Gutenberg (independente do Elementor)
- `list-blocks`/`get-block-schema` (batch via `names[]`) → `add-block` (markup bruto, `position.mode: append|prepend|before|after|inside`) é o padrão descobrir → schema → aplicar.
- `get-post-blocks` devolve um **PATH em índice por bloco** (ex. `[2,1]`) — chamar sempre isto imediatamente antes de `update-block`/`remove-block`/`move-block`/`duplicate-block` para obter os paths actuais; os paths mudam após cada edição, não fazer cache entre chamadas.
- `list-patterns`/`insert-pattern` para composições de blocos pré-construídas.
- Estas tools operam sobre `post_content`, por isso são a camada certa para posts/CPTs clássicos que não usam Elementor — não para páginas Elementor (`is_elementor: true`), que não têm árvore de blocos Gutenberg significativa.
## Pesquisa/reutilização de conteúdo
`search-content(query)` — pesquisa tipo linguagem natural sobre páginas/templates/widgets/estilos globais, ordenada por relevância. Faz correspondência a substrings/tokens literais no conteúdo indexado, não pesquisa semântica difusa — uma query com uma palavra ausente verbatim de qualquer título/conteúdo devolve zero resultados mesmo que o conceito esteja presente. Se os resultados parecerem desactualizados (edições recentes não reflectidas), chamar `reindex-search` primeiro — o índice também actualiza incrementalmente ao gravar, por isso um reindex completo só é preciso na primeira construção ou para forçar um refresh. Usar isto antes de construir conteúdo novo, para encontrar e reutilizar/clonar uma página/template/widget existente em vez de duplicar trabalho.
## Plugins / Temas / Utilizadores / Definições (nível só-leitura, quase universal)
- `list-plugins`/`search-plugins`, `list-themes`/`search-themes`, `list-users`/`get-user` — listagem e pesquisa comummente activas; instalar/activar/desactivar/actualizar/eliminar plugins ou temas, e criar/actualizar utilizadores, estão comummente no deny-list (categorias de maior risco depois de execução de código). Verificar o que está realmente montado antes de prometer que uma mutação é possível.
- `get-settings`/`update-settings` — um subconjunto curado e allowlisted de definições WordPress (Geral/Leitura/Escrita/Discussão/Media/Permalinks), não `wp_options` bruto. `update-settings` reporta chaves ignoradas (`skipped`) com a razão (só-leitura, não allowlisted, valor inválido) em vez de falhar todo o lote — verificar o array `skipped`, não assumir que todas as chaves passadas foram aplicadas.
- `theme-read`/`theme-write` e leitores específicos de framework (ex. `astra-read`, quando o Astra é o tema activo — só regista se esse tema estiver activo) são **tools despachadoras** também: chamar sem `operation` primeiro para descobrir o que está disponível no tema específico desse site.
## Referência cruzada
- `emcp-page-building` — construção/edição específica de Elementor (atómico + legacy).
- `emcp-site-audit` — scan de segurança, análise de performance, acesso só-leitura a BD/filesystem, ledger de mudanças + rollback.
@@ -0,0 +1,73 @@
---
name: emcp-page-building
description: Construção e edição de páginas Elementor via qualquer ligação MCP EMCP Tools (emcp-descomplicar, emcp-emanuelalmeida, emcp-tools/starter, ou outra). Cobre elementos atómicos Elementor 4.0+, containers/widgets legacy, templates, operações em lote, e as armadilhas reais de parâmetros/comportamento encontradas por teste ao vivo. Usar quando "construir página elementor", "emcp page building", "widget atómico", "add-flexbox", "apply-template", "criar página com mcp".
layer: wiki
---
# /emcp-page-building — Construção de Páginas Elementor via EMCP Tools
Aplica-se a **qualquer** ligação MCP EMCP Tools, não a um site específico. Confirma sempre o site alvo com `core/get-site-info` antes de escrever — nunca assumas pelo nome da ligação.
Factos abaixo verificados ao vivo contra uma instalação EMCP Tools 3.12.1 em produção (Elementor 4.2.2 + Pro), revertidos depois via ledger. Descrevem o comportamento do **plugin**, por isso generalizam a qualquer site com a mesma versão — reverifica numa página de rascunho descartável se o site tiver uma versão materialmente diferente.
## Ordem de descoberta (antes de construir algo desconhecido)
1. `detect-elementor-version` — confirma suporte atómico + `recommended_mode` (`"atomic"` em sites 4.0+, usar tools atómicas por omissão).
2. `list-widgets` (opcional `tier: free|pro|woo`, `category`, ou pesquisa livre) — catálogo curado nativo Elementor/Pro/Woo. **Não inclui widgets de terceiros** (ElementsKit, PowerPack) — ver skills `elementskit-widgets` e `powerpack-widgets`.
3. `get-widget-schema({ widget_type })` — **o parâmetro é `widget_type`, não `type`.** Nome errado falha com um erro genérico de "propriedade obrigatória" que não diz qual campo.
4. `get-container-schema` — schema completo de controlos de container (flex + grid).
5. `get-page-structure({ post_id })` antes de tocar numa página existente — conhece sempre a árvore actual antes de mutar.
## Atómico (Elementor 4.0+) vs legacy — coexistem na mesma página
Confirmado ao vivo: uma página com um `e-flexbox` atómico (com widgets `e-heading`/`e-paragraph` filhos) ao lado de um `container` legacy (com um widget `heading` legacy) gravou e renderizou a estrutura correctamente. Não é preciso escolher um modo por página.
- Containers atómicos: `add-flexbox`, `add-div-block`. Widgets atómicos só ligam a estes (ou a outro container atómico) como `parent_id` — `add-atomic-heading` etc. rejeitam um `container` legacy como pai.
- Containers legacy: `add-container`. Widgets legacy (`add-free-widget`, ex. `widget_type: "heading"`) só ligam a containers legacy.
- Usa legacy para qualquer widget sem equivalente atómico ainda — confirmado ao vivo: widgets de terceiros como `elementskit-*`/`elementskit-wp-forms` só existem como widgets legacy.
- `update-element` funciona em elementos atómicos e legacy genericamente. `update-atomic-widget` é a variante de merge parcial específica para atómicos — preferir para atómicos, mas `update-element` é o fallback universal seguro.
- `batch-update({ post_id, operations: [{element_id, settings}, ...] })` actualiza vários elementos (atómicos ou legacy, misturados) numa só gravação — preferir sempre isto a N chamadas sequenciais de `update-element`.
## 🔴 Armadilhas confirmadas ao vivo — não confiar só na descrição da tool
1. **`set-element-label`**: o campo real obrigatório é `title`, não `label`, apesar da descrição "Sets an element's Navigator label" sugerir `label`. Ler o schema (`read xd://mcp__<ligação>_emcp_tools_set_element_label`) antes da primeira utilização numa ligação nova.
2. **`add-custom-js`**: exige `parent_id` (tem de ser um container **legacy** — insere um widget HTML) e o campo de código é `js`, não `code`. Não liga a um `e-flexbox`/`e-div-block` atómico.
3. **`apply-template` pode substituir TODO o conteúdo existente da página em vez de inserir.** Testado: chamar `apply-template(post_id, template_id)` sem `position` explícito numa página com 7 elementos existentes resultou em substituição total — só ficaram os elementos do template (`elements_added` igual à contagem própria do template). A descrição sugere inserção ("at a given position, inserting its elements"); o comportamento observado foi sobrescrita. **Sequência obrigatória:** `get-page-structure` antes → `apply-template(..., position: -1)` explícito → `get-page-structure` depois, comparar. Se o conteúdo foi apagado, recuperar via `rollback-change` na entrada mais recente de `list-changes` para esse `post_id` (ver skill `emcp-site-audit`) — confirmado a funcionar.
4. **`create-theme-template` fica publicado de imediato**, não como rascunho. Testado: `create-theme-template(type:"single", title:"...")` devolveu `status: "publish"` com `conditions.include: [{"object":"all-singular"}]` por omissão — com conteúdo vazio, aplicar-se-ia de imediato a todo o singular do site. Chamar logo `set-template-conditions` para restringir o alcance antes de construir conteúdo, ou `delete-theme-template` de imediato se criado por engano (confirmado limpo/reversível).
5. **`get-element-settings` normaliza os wrappers `$$type` atómicos; `get-page-structure` e `export-page` não.** Para o mesmo heading atómico, `get-element-settings` devolve `{"title": "string simples"}` enquanto os outros dois mostram a estrutura crua `{"$$type":"html-v3","value":{"content":{"$$type":"string","value":"..."}}}`. Usar `get-element-settings` para "qual é o valor agora"; usar `get-page-structure`/`export-page` para entender ou reproduzir a estrutura crua de prop-types atómicos.
6. **`set-post-terms` com `create_missing:true` deixa termos de taxonomia permanentes** — sem tool de eliminação de termos no conjunto activo em nenhum site testado até agora. Confirmar o nome do termo antes de usar em conteúdo descartável/teste.
## Construir uma página de raiz — sequência recomendada
1. `create-page({ title, status: "draft" })` — nunca construir directamente numa página publicada.
2. `add-flexbox({ post_id, settings: { flex_direction, gap } })` (ou `add-div-block`) para o primeiro container estrutural. `add-container` só se precisares especificamente de comportamento legacy ou nesting sob uma árvore legacy existente.
3. Aninhar widgets atómicos: `add-atomic-heading`/`add-atomic-paragraph`/`add-atomic-button`/`add-atomic-image`/`add-atomic-svg`/`add-atomic-video`/`add-atomic-youtube`/`add-atomic-divider`, ou o genérico `add-atomic-widget` para o que não tenha tool de conveniência.
4. Para qualquer widget legacy necessário (Pro/terceiros ainda sem versão atómica, `add-pro-widget` se o deny-list do site permitir): `add-container` depois `add-free-widget`.
5. Verificar com `get-page-structure` após cada passo relevante, não só no fim.
6. `reorder-elements`, `move-element`, `duplicate-element`, `remove-element` operam por ID de elemento das chamadas de structure/find-element — nunca adivinhar IDs.
7. `set-element-label({ post_id, element_id, title })` para manter o Navigator legível em páginas complexas (campo `title`, ver armadilha #1).
8. `update-page-settings` para definições de página ao nível Elementor (background, padding, CSS custom — funcionalidades Pro degradam de forma graciosa se indisponíveis).
9. Antes de publicar: `get-page-snapshot` para um único digest normalizado (estrutura + contagens + cores/tipografia globais em uso + resumo SEO-lite) em vez de encadear chamadas separadas de structure/global-settings/classes.
## Tokens de design globais
- `get-global-settings` — kit de todo o site (cores, tipografia, espaçamento, breakpoints).
- `update-global-colors`/`update-global-typography` — muta o kit directamente; afecta todas as páginas que usam esses tokens, confirmar com o utilizador primeiro.
- `list-global-classes` — resolve IDs `g-` do Class Manager do Elementor 4.0 para nomes/CSS legíveis. Normalmente só-leitura; `create-global-class`/`update-global-class`/`delete-global-class`/`reorder-global-classes` estão comummente no deny-list (mutação de CSS partilhado) — verificar se estão montadas antes de assumir que funcionam.
## Templates
- `list-templates` (templates guardados na biblioteca Elementor, qualquer tipo: page/section/container) e `save-as-template` para criar novos. **Não há tool de eliminação de template no conjunto comummente activo em nenhum site testado** — guardar um template é efectivamente permanente via MCP. Obter confirmação explícita antes de `save-as-template`.
- `import-template`/`export-page` para estruturas JSON portáveis.
- Theme Builder (`create-theme-template`/`list-theme-templates`/`get-theme-template`/`update-theme-template`/`set-template-conditions`/`resolve-template`/`delete-theme-template`) — ver armadilha #4 acima. `list-condition-targets` antes de `set-template-conditions` para ver selectores/post-types/taxonomias válidos naquele site.
## Padrão de teste seguro
Nunca experimentar numa página publicada real. `create-page(status:"draft")`, construir/mutar livremente, confirmar via `get-page-structure`, depois `delete-post(post_id)` (vai para o Lixo — reversível; só `force:true` se o utilizador pedir explicitamente remoção permanente). Verificar `list-changes` depois — toda a escrita aqui fica registada no ledger, por isso um teste falhado é sempre recuperável com `rollback-change` mesmo antes de chegares a apagar o rascunho.
## Skills relacionadas
- `emcp-content-ops` — operações de conteúdo WordPress (posts/pages/media/menus/redirects/blocos).
- `emcp-site-audit` — scan de segurança, análise de performance, acesso só-leitura a BD/filesystem, ledger de mudanças.
- `elementor-pro-widgets` — catálogo completo de widgets nativos Elementor Pro.
- `elementskit-widgets` / `powerpack-widgets` — catálogos de widgets de terceiros (não estão no catálogo curado do EMCP).
- `emcp-tools` — arquitectura do plugin, inventário completo de ~162 abilities, mecanismo de deny-list.
+58
View File
@@ -0,0 +1,58 @@
---
name: emcp-site-audit
description: Auditoria de segurança, performance, base de dados e filesystem de um site WordPress via qualquer ligação MCP EMCP Tools, e recuperação de uma edição de IA falhada via o ledger de mudanças/rollback. Usar quando "scan-security mcp", "analyze-performance mcp", "rollback-change", "ledger de mudanças elementor", "falso positivo malware uploads", "query base de dados via mcp".
layer: wiki
---
# /emcp-site-audit — Auditoria e Rede de Segurança via EMCP Tools
Aplica-se a qualquer ligação MCP EMCP Tools. Confirma o site alvo com `core/get-site-info`.
## Scan de segurança
`scan-security` — só-leitura, autocontido, audita apenas o site ligado. Quatro áreas: heurísticas de malware PHP (uploads + plugins/temas activos; `deep:true` para a árvore completa), integridade de ficheiros do core WordPress (vs checksums oficiais), hardening de configuração (editor de ficheiros activo, output de debug, username admin, XML-RPC, divulgação de versão, HTTPS, security headers), e software desactualizado/abandonado. Devolve `{summary: {score, grade, counts: {critical, warning, pass, info}}, sections: {...}}`.
**Tratar um achado `critical` como um reporte imediato e separado ao utilizador — nunca enterrado dentro de uma tarefa não relacionada.** Mas **`malware_uploads_php` (qualquer `.php` sob `wp-content/uploads/`, categoria `critical`) é uma heurística grosseira que assinala só a localização, não o conteúdo — tem um padrão confirmado e comum de falso positivo. Ler sempre o ficheiro assinalado com `read-file` e inspeccionar o conteúdo real antes de reportar como ameaça real.**
Confirmado benigno num scan ao vivo (dois hits `critical`, mesmo id de achado, ambos falsos positivos):
- `wp-content/uploads/index.php` — stub próprio do **core WordPress**, incluído por omissão em toda a instalação: `header($_SERVER['SERVER_PROTOCOL'].' 403 Forbidden'); die('403 Forbidden');`. Existe só para bloquear listagem de directório. **Nunca apagar este** — removê-lo *reduz* a segurança ao remover exactamente a protecção que fornece.
- `wp-content/uploads/<plugin>/cache/index.php` (visto: WPForms) — mesmo padrão, incluído pelo plugin, ex. `header(...' 404 Not Found');`. Qualquer plugin que faça cache em `uploads/` tipicamente inclui um destes por directoria de cache.
Regra de diagnóstico: um ficheiro `uploads/*.php` genuinamente malicioso contém lógica executável real (`eval`, `base64_decode`, `system`/`exec`/`shell_exec`, payloads ofuscados, marcadores de webshell) — um stub benigno tem 2-4 linhas, só `header()` + `die()`/`exit` opcional, nada mais. Se `read-file` mostrar só um stub de header/die, despromover o achado para informativo no reporte e **não** apagar o ficheiro. Se mostrar outra coisa — ameaça real, reportar de imediato e confirmar com o utilizador antes de apagar (o caminho de remoção confirmado seguro é `delete-file` se montada para a ligação, ou `wp eval`/`rm` via SSH quando não estiver — ver secção Filesystem abaixo).
Achados `integrity_modified` (ficheiro do core difere dos checksums oficiais) também podem ser rotina — ex. `wp-includes/version.php` difere por desenho (é o próprio ficheiro de versão instalada do WordPress, espera-se que difira de uma baseline de checksum genérica). Cruzar o que mudou (`read-file` no ficheiro assinalado) antes de tratar um achado de integridade como indicador de comprometimento.
## Análise de performance
`analyze-performance` — só-leitura. Analisa configuração do servidor, internals WordPress (tamanho da BD, opções autoloaded, backlog de cron, object cache, OPcache, contagem de plugins), e uma página alvo (por omissão a página inicial; passar `url` ou `post_id` para uma página específica). Devolve a mesma forma `{summary: {score, grade, counts}, sections}` que `scan-security`, com `status` por achado (`pass`/`warning`/`critical`), `message`, e `recommendation` — uma lista de acções priorizada já pronta, não reportar só o score.
## Base de dados (nível só-leitura)
`list-tables` (contagens de linhas + tamanhos), `describe-table({ table })` (colunas/tipos/keys), `query({ sql })` — **só SELECT/SHOW/DESCRIBE/EXPLAIN, escritas e DDL são rejeitadas no servidor.** Resultados têm limite — para conjuntos grandes, restringir a query em vez de esperar paginação. O prefixo das tabelas varia por instalação (visto ao vivo: `wpne_` num site) — sempre `list-tables` primeiro em vez de assumir `wp_`.
`insert-row`/`update-rows`/`delete-rows` existem no plugin mas estão comummente no deny-list em todos os sites de produção testados até agora — escritas SQL directas contornam a camada de validação do próprio WordPress. Não tentar sem confirmar primeiro que estão realmente montadas para essa ligação.
## Filesystem (nível só-leitura)
`list-directory`/`read-file`/`search-files`, confinados à raiz da instalação WordPress. `read-file` recusa ficheiros de segredos no servidor — confirmado ao vivo: pedir `wp-config.php` devolve *"This file holds site secrets ... and cannot be read"* em vez de um erro ou do conteúdo, independentemente da variante de caminho tentada. `search-files({ query, extensions, path })` faz grep file:line numa árvore de directórios, com limite — bom para confirmar versão de plugin/tema ou um padrão de código específico sem SSH.
`write-file`/`edit-file`/`delete-file` existem no plugin mas estão quase universalmente no deny-list (escritas directas no filesystem contornam toda a auditoria de shell/SSH) — tratar como indisponível salvo confirmação explícita de que está montada. Quando um ficheiro confirmadamente malicioso precisa de ser removido e `delete-file` não está montada, usar o canal SSH/wp-cli próprio do site em vez de forçar via MCP.
## Ledger de mudanças + rollback — a rede de segurança para toda escrita Elementor/conteúdo
Toda a chamada mutante que este plugin expõe (edições Elementor, e nalguns sites também escritas de filesystem/BD) fica registada. Testado ao vivo, ponta-a-ponta, e funciona:
1. `list-changes` (opcional filtrar por `domain`: elementor/filesystem/database, `rolled_back`, `reversible`) — mais recente primeiro, cada entrada tem `id`, `summary`, `reversible`, `rolled_back`.
2. `get-change({ id })` para detalhe completo incluindo a referência de rollback (before-image / ponteiro de backup / `after_hash`).
3. `rollback-change({ id })` — reverte essa mudança. Confirmado ao vivo: reverteu uma página exactamente para a árvore de elementos anterior, removendo precisamente o que aquela mudança tinha adicionado e nada das mudanças à volta. Recusa com erro de conflito se o alvo mudou de novo desde a mudança registada (passar `force:true` para sobrepor e aceitar perda de dados no estado mais recente). Nunca faz double-rollback da mesma entrada; regista uma entrada compensatória no ledger quando corre.
**Quando qualquer escrita nesta sessão correr mal ou produzir um resultado inesperado (ver a armadilha de sobrescrita do `apply-template` em `emcp-page-building`), recorrer a `list-changes` → `rollback-change` antes de tentar reconstruir manualmente o estado anterior.** É mais rápido e fiável do que reconstruir a partir do output de `get-page-structure`.
## Espelho de conteúdo (export/restore git-friendly — separado do ledger de mudanças)
`export-content`/`list-content-exports`/`restore-content` — exporta conteúdo de página/template Elementor para ficheiros JSON sob `uploads/emcp-content-mirror/` para um VCS externo poder fazer diff/versionar designs, e restaura a partir de um ficheiro de espelho (um undo baseado em ficheiro, distinto do ledger acima). `list-content-exports` mostra o que está actualmente espelhado em disco. Útil antes de um lote grande de trabalho de construção de páginas como checkpoint extra além do ledger automático.
## Referência cruzada
- `emcp-page-building` — construção de páginas Elementor, incluindo as tools com maior probabilidade de precisar de rollback (`apply-template`, `create-theme-template`).
- `emcp-content-ops` — operações de conteúdo/media/menus/redirects WordPress.
- `emcp-tools` (skill genérica) — inventário completo de ~162 abilities e o mecanismo de deny-list que decide quais das tools referidas acima estão realmente montadas numa dada ligação.
+561
View File
@@ -0,0 +1,561 @@
---
name: emcp-tools
description: Auditoria e gestão de postura de segurança do EMCP Tools (extensão do WP MCP Adapter da Elementskit — expõe dados Elementor, widgets e ferramentas de operação como MCP tools para agentes de IA). Cobre inventário completo das ~162 abilities registadas por categoria (activas vs desligadas, verificado via wp_get_abilities()), os 9 módulos do plugin (prompts, brand-kits, templates, themer, redirects, agent-skills, cloud, image-optimization, svg-support) e o que cada um expõe, o mecanismo de deny-list incremental versionado (34 migrações), activação granular e segura de uma ferramenta individual (ex: só leitura Rank Math), avaliação de risco por categoria, e arquitectura de registo MCP multi-site. Usar quando "emcp tools", "emcp-tools", "mcp adapter wordpress", "ferramentas mcp elementor", "que tools o agente pode chamar", "activar tool emcp", "desligar tool mcp", "postura default-deny mcp", "agentes de ia no wordpress", "módulos emcp tools", "inventário completo mcp tools", "quantas tools estão activas".
---
# /emcp-tools — Auditoria e Postura de Segurança do EMCP Tools (MCP para Elementor/WP)
Plugin `emcp-tools` (Mian Shahzad Raza) — extensão do WP MCP Adapter que expõe
dados Elementor, widgets, page design tools e um conjunto amplo de
operações WordPress como **ferramentas MCP** chamáveis por agentes de IA.
Versão verificada `3.12.1`, instalado e activo em `emanuelalmeida.pt`,
`descomplicar.pt` (`public_html`) e `starter.descomplicar.pt`.
**Fonte:** `Hub/04-Stack/02.04-Sistemas/71.Seguranca/CONFIG-Plugins-Referencia.md`
§10 (EMCP Tools) + `BUNDLE-Excelencia-WP.md` + verificação SSH ao vivo nesta
sessão (16-08-2026) nos três sites do ecossistema.
---
## Arquitectura — como o agente chega às tools deste plugin
O plugin regista um servidor MCP próprio, `emcp-tools-server` (nome visível
"MCP Tools for Elementor Server"), lado a lado com outros servidores do
mesmo WP MCP Adapter (`elementskit-mcp-server`, `mcp-adapter-default-server`,
e nalguns sites `fluent-crm`). A ligação do agente ao site faz-se por SSH +
STDIO, uma entrada por site em `~/.omp/agent/mcp.json`:
```json
"emcp-emanuelalmeida": {
"type": "stdio",
"command": "ssh",
"args": ["-T", "-o", "BatchMode=yes", "server",
"sudo -u ealmeida /usr/local/bin/wp mcp-adapter serve --server=emcp-tools-server --user=ealmeida --path=/home/ealmeida/emanuelalmeida.pt"],
"timeout": 60000,
"enabled": true
}
```
**Três instâncias activas no ecossistema**, uma ligação MCP por site (cada
uma com o seu próprio `--path` e, portanto, a sua própria config
`emcp_tools_*` independente — **não presumir que a postura de um site se
aplica a outro**, ver §3):
| Ligação em `mcp.json` | Site | `--path` |
|---|---|---|
| `emcp-descomplicar` | descomplicar.pt | `/home/ealmeida/public_html` |
| `emcp-tools` | starter.descomplicar.pt | `/home/ealmeida/starter.descomplicar.pt` |
| `emcp-emanuelalmeida` | emanuelalmeida.pt | `/home/ealmeida/emanuelalmeida.pt` |
Comando nativo WP-CLI para inspecção do lado do servidor (sem passar pelo
protocolo MCP):
```bash
wp mcp-adapter list --path=$PATH --allow-root
wp mcp-adapter list --format=json --path=$PATH --allow-root
```
⚠️ **Achado desta sessão — a coluna `Tools` de `wp mcp-adapter list` NÃO é
um proxy fiável do nº de ferramentas realmente activas.** Verificado ao
vivo nos 3 sites (16-08-2026):
| Site | `emcp_tools_disabled_tools` (desligadas) | `wp mcp-adapter list` → coluna `Tools` do `emcp-tools-server` | Activas REAIS (`wp_get_abilities()`, §7) |
|---|---|---|---|
| descomplicar.pt | 78 | `3` | **165** (achado crítico — §8.2) |
| starter.descomplicar.pt | 131 | `3` | 114 |
| emanuelalmeida.pt | 131 | `115` | 115 |
Não há correlação directa (menos desligadas em `descomplicar.pt` devia
implicar mais activas, mas mostra o valor mais baixo). A contagem desta
coluna parece depender de estado de registo/cache do lado do WP-CLI, não do
filtro `emcp_tools_disabled_tools` aplicado em tempo de pedido MCP. **Não
usar esta coluna para avaliar postura de segurança** — usar sempre o
conteúdo real de `emcp_tools_disabled_tools` (§2) para saber o que está
fechado, e o protocolo MCP (`tools/list` via um cliente MCP ligado, ex. a
própria sessão do agente) para saber com exactidão o que está disponível
para chamada num momento dado. A 4ª coluna acima é a contagem definitiva,
obtida com o método de §7 (`wp eval` + `wp_get_abilities()` filtrado, cruzado
slug-a-slug contra `emcp_tools_disabled_tools`) — coincide exactamente com a
coluna `Tools` só em `emanuelalmeida.pt`; nos outros dois sites a coluna do
WP-CLI é enganadora nos dois sentidos (mostra `3` quando o real é 114 **e**
165).
---
## 1. Config verificada (`emanuelalmeida.pt`, `wp_options`)
```bash
wp option list --search='emcp_tools*' --path=$PATH --allow-root
```
| Opção | Valor real | Nota |
|---|---|---|
| `emcp_tools_sandbox_location` | `emcp-sandbox` | Directório isolado para operações de sandbox |
| `emcp_tools_active_modules` | `["prompts","brand-kits","templates","themer","redirects","agent-skills","cloud"]` (7 de 9 semeados) | `image-optimization` e `svg-support` estão **semeados mas não activos** — idêntico nos 3 sites verificados |
| `emcp_tools_disabled_tools` | array indexado com **131** ferramentas desligadas (formato `emcp-tools/<nome-tool>`) | Ver lista completa §2 |
| `emcp_tools_defaults_applied` | `34` | Contador interno de defaults aplicados na instalação |
| `emcp_tools_modules_seeded` / `emcp_tools_legacy_meta_migrated` / `emcp_tools_slug_namespace_migrated` | flags internas de migração | Sem acção |
| `emcp_tools_changelog_db_version` / `emcp_tools_oauth_db_version` / `emcp_tools_redirects_db_version` / `emcp_tools_search_index_db_version` | versões de schema interno | Sem acção |
| `emcp_tools_themer_index` / `emcp_tools_themer_index_healed` | índice interno do módulo Themer | Sem acção |
Outros dois sites (`descomplicar.pt`, `starter.descomplicar.pt`) partilham
o mesmo `emcp_tools_active_modules`; `starter.descomplicar.pt` partilha
exactamente os mesmos 131 nomes desligados que `emanuelalmeida.pt`;
`descomplicar.pt` tem apenas 78 desligados (config mais permissiva —
**verificar antes de assumir postura igual entre sites**). Note-se também
que `templates` e `agent-skills` são módulos **Pro** (`tier() === 'pro'`)
semeados como "activos" em todos os 3 sites por omissão do próprio plugin,
mas nenhum destes sites tem licença Pro (`emcp_tools_fs()->can_use_premium_code()
=== false`), logo `is_available()` devolve `false` e **nenhum dos dois
módulos corre de facto** — ver tabela completa dos 9 módulos em §6, e o
achado crítico específico de `descomplicar.pt` em §8.2 (78 desligados não
significa "mais permissivo apenas um pouco" — significa **zero** tools
nativas fechadas).
---
## 2. Lista completa das 131 ferramentas desligadas por omissão
Confirmada ao vivo (`emanuelalmeida.pt` e `starter.descomplicar.pt`,
formato idêntico): array indexado (não associativo) em
`emcp_tools_disabled_tools`, cada entrada `emcp-tools/<slug-da-tool>`.
```bash
wp option get emcp_tools_disabled_tools --format=json --path=$PATH --allow-root
```
### 2.1 Ficheiros, sistema e execução remota — risco máximo, nunca activar sem supervisão directa
```
write-file, edit-file, delete-file, run-wp-cli, dispatch-wp-cli,
get-wp-cli-job, list-wp-cli-jobs
```
### 2.2 Base de dados directa
```
insert-row, update-rows, delete-rows
```
### 2.3 Gestão de plugins e temas
```
install-plugin, activate-plugin, deactivate-plugin, update-plugin, delete-plugin,
install-theme, switch-theme, update-theme, delete-theme
```
### 2.4 Utilizadores
```
create-user, update-user
```
### 2.5 Redirecções e migração de site
```
create-redirect, update-redirect, delete-redirect,
migrate-site, sync-to-live, sync-content-item
```
### 2.6 Integrações de terceiros — leitura e escrita (granularidade por par read/write)
```
acf-write
woo-read, woo-write
metabox-write
essential-addons-read, premium-addons-read, uae-read, uae-write
cf7-write
wpforms-read, wpforms-write
gravityforms-read, gravityforms-write
fluentforms-read, fluentforms-write
ninjaforms-read, ninjaforms-write
formidable-read, formidable-write
metform-read, metform-write
sureforms-read, sureforms-write
forminator-read, forminator-write
```
### 2.7 SEO — TODOS os plugins, leitura E escrita fechadas (Rank Math dono único da superfície SEO neste bundle)
```
slimseo-write
yoast-read, yoast-write
rankmath-read, rankmath-write
aioseo-read, aioseo-write
seopress-read, seopress-write
seoframework-read, seoframework-write
surerank-read, surerank-write
```
### 2.8 Temas/frameworks de blocos
```
theme-write
astra-write
spectra-write
kadence-write, kadence-blocks-write
generatepress-read, generatepress-write
generateblocks-read, generateblocks-write
blocksy-blocks-read, blocksy-blocks-write, blocksy-extensions-read, blocksy-extensions-write
```
### 2.9 Elementor avançado (widgets Pro, templates de tema, popups, dynamic tags, CSS/classes globais)
```
add-pro-widget, create-elementor-theme-template, set-elementor-template-conditions,
list-dynamic-tags, set-dynamic-tag, create-popup, set-popup-settings,
delete-media, add-custom-css,
create-global-class, update-global-class, delete-global-class, reorder-global-classes
```
### 2.10 Snippets PHP — execução de código arbitrário, risco máximo absoluto
```
add-code-snippet, list-code-snippets,
validate-php-snippet, create-php-snippet, update-php-snippet, get-php-snippet,
list-php-snippets, delete-php-snippet
```
### 2.11 Memória do agente
```
recall, remember, save-session-summary
```
### 2.12 Templates PHP de tema
```
create-theme-php-template, list-theme-php-templates, get-theme-php-template,
update-theme-php-template, delete-theme-php-template
```
### 2.13 SEO/A11y automatizados (geração de conteúdo/schema/contraste)
```
audit-page-seo, extract-keywords-from-content, generate-meta-tags,
generate-schema-markup, set-social-image,
audit-page-a11y, fix-color-contrast, add-alt-text-from-context
```
### 2.14 Widgets e blocos custom (criação/edição/eliminação)
```
list-control-types, validate-widget-spec,
create-custom-widget, update-custom-widget, get-custom-widget, list-custom-widgets,
set-widget-status, delete-custom-widget,
list-block-control-types, validate-block-spec,
create-custom-block, update-custom-block, get-custom-block, list-custom-blocks,
set-block-status, delete-custom-block
```
---
## 3. Como listar ferramentas activas vs desligadas num site
```bash
PATH=/home/USER/SITE
# Quantas e quais estão desligadas
wp option get emcp_tools_disabled_tools --format=json --path=$PATH --allow-root
# Módulos activos (nível mais alto — prompts/brand-kits/templates/themer/redirects/agent-skills/cloud/image-optimization/svg-support)
wp option get emcp_tools_active_modules --format=json --path=$PATH --allow-root
# Servidores MCP registados neste site (não usar a coluna Tools como medida de activo — ver aviso acima)
wp mcp-adapter list --path=$PATH --allow-root
```
**Para saber com exactidão quais tools estão disponíveis para chamada num
dado momento** (a lista real, não a config): ligar via o protocolo MCP
(a própria ligação `emcp-*` em `mcp.json`) e pedir `tools/list` — a config
`emcp_tools_disabled_tools` é aplicada como filtro em tempo de pedido pelo
plugin, não é reflectida de forma fiável pelo WP-CLI.
---
## 4. Activar uma ferramenta específica com segurança (granularidade individual)
O array é **indexado** (não associativo), por isso `wp option patch` não
serve para remover/adicionar uma entrada isolada — usar `wp eval` com
`array_diff`/`array_values` para manter o array limpo e sem duplicados.
### Activar (remover do deny-list)
```bash
wp eval '
$tool = "emcp-tools/rankmath-read";
$disabled = get_option("emcp_tools_disabled_tools", []);
$before = count($disabled);
$disabled = array_values(array_diff($disabled, [$tool]));
update_option("emcp_tools_disabled_tools", $disabled);
printf("antes=%d depois=%d removida=%s\n", $before, count($disabled), $before !== count($disabled) ? "sim" : "nao (ja estava activa ou nome errado)");
' --allow-root --path=$PATH
```
**Exemplo do pedido típico — activar só leitura Rank Math sem abrir
escrita:** activar `emcp-tools/rankmath-read` e deixar
`emcp-tools/rankmath-write` no array (continua desligada). A granularidade
é sempre ao nível do slug individual — nunca existe um toggle "Rank Math
on/off" only por read/write, exactamente como aparece na lista §2.7.
### Reverter (voltar a fechar)
```bash
wp eval '
$tool = "emcp-tools/rankmath-read";
$disabled = get_option("emcp_tools_disabled_tools", []);
if (!in_array($tool, $disabled, true)) { $disabled[] = $tool; }
update_option("emcp_tools_disabled_tools", $disabled);
echo count($disabled) . " tools desligadas\n";
' --allow-root --path=$PATH
```
### Verificar sempre depois, de forma independente
```bash
wp option get emcp_tools_disabled_tools --format=json --path=$PATH --allow-root | grep -o 'rankmath-read' || echo "confirmado: activa (nao esta no deny-list)"
```
---
## 5. Avaliação de risco por categoria — o que activar e o que nunca activar sem supervisão
| Categoria | Postura recomendada | Justificação |
|---|---|---|
| Leitura de design Elementor (o que fica activo por omissão — não está no deny-list) | ✅ Seguro por omissão | Propósito central do plugin; só leitura/inspecção, sem mutação |
| SEO leitura isolada (ex.: só `rankmath-read`) | ✅ Seguro activar caso a caso | Não muta nada; útil para um agente auditar sem escrever — mas Rank Math continua a ser gerido preferencialmente via `/rank-math` (WP-CLI directo), não via MCP |
| SEO escrita (qualquer `*-write` de SEO) | 🔴 Nunca activar sem supervisão | Contorna o fluxo de auditoria/rollback do `/rank-math`; risco de meta/schema incoerente sem revisão humana |
| Redirecções/migração (`create-redirect`, `migrate-site`, `sync-to-live`) | 🔴 Nunca activar sem supervisão | Mutação estrutural com impacto em produção e SEO (redirects mal configurados = perda de ranking) |
| Gestão de plugins/temas (`install-plugin`, `activate-plugin`, …) | 🔴 Nunca activar sem supervisão | Um agente autónomo instalar/remover plugins é a superfície mais perigosa depois de execução de código |
| Snippets PHP (`create-php-snippet`, `validate-php-snippet`, `update-php-snippet`) | 🔴🔴 Nunca activar — risco máximo absoluto | **Execução de PHP arbitrário** persistido no site; equivalente a dar shell ao agente. Único candidato a "nunca, em circunstância nenhuma, sem revisão humana linha a linha antes de guardar" |
| Ficheiros/sistema (`write-file`, `edit-file`, `delete-file`, `run-wp-cli`, `dispatch-wp-cli`) | 🔴🔴 Nunca activar — risco máximo | Escrita directa no filesystem e execução WP-CLI arbitrária via MCP; contorna toda a auditoria de shell/SSH existente |
| Base de dados directa (`insert-row`, `update-rows`, `delete-rows`) | 🔴🔴 Nunca activar — risco máximo | SQL de escrita sem passar por validação da camada WP; risco de corrupção silenciosa |
| Widgets/blocos custom (`create-custom-widget`, `create-custom-block`, …) | 🟡 Só com supervisão activa | Cria código PHP/JS executado no frontend; menos grave que snippets PHP mas ainda superfície de execução |
| Utilizadores (`create-user`, `update-user`) | 🔴 Nunca activar sem supervisão | Escalada de privilégios / criação de contas fora do fluxo humano |
| Memória do agente (`recall`, `remember`, `save-session-summary`) | 🟡 Avaliar por site | Sem risco de segurança directo, mas cria estado persistente fora do controlo humano — activar só se o workflow do site precisar de continuidade entre sessões MCP |
| Integrações de terceiros — leitura (`woo-read`, `wpforms-read`, …) | 🟡 Seguro activar caso a caso, um a um | Só expõe dados existentes; activar apenas as integrações realmente instaladas no site, nunca em bloco |
| Integrações de terceiros — escrita (`woo-write`, `cf7-write`, …) | 🔴 Nunca activar sem supervisão | Mutação em dados de negócio (encomendas, submissões de formulário) sem revisão humana |
**Regra geral (revista após leitura completa do código-fonte, §6-§8):** a
**concepção** do plugin é default-deny bem desenhada — o deny-list incremental
descrito em §8.1 fecha metodicamente cada nova categoria de risco assim que é
lançada. Mas a **postura real, verificada ao vivo, NÃO é uniforme entre os 3
sites**: `emanuelalmeida.pt` e `starter.descomplicar.pt` reflectem essa
concepção (115/162 e 114/160 tools activas, respectivamente, com todas as
categorias 🔴/🔴🔴 de facto fechadas); `descomplicar.pt` diverge de forma
crítica — **165/165 tools registadas estão activas**, incluindo filesystem,
base de dados directa, WP-CLI, PHP snippets e gestão de plugins/utilizadores
(ver achado detalhado em §8.2). Qualquer activação deve ser: (1) ao nível de
uma única tool, nunca em bloco; (2) reversível e verificada de imediato (§4,
com o método correcto de §7/§8.3 — nunca por contagem de arrays); (3)
documentada aqui ou em nota do site, para não perder rasto de porque é que
uma excepção existe.
---
## 6. Módulos — os 9 blocos que a página *Modules* liga/desliga
Módulo ≠ ferramenta individual: é um interruptor de alto nível (`emcp_tools_active_modules`)
que liga/desliga uma funcionalidade inteira (CPT, hooks de front-end, tab de admin e, nalguns
casos, o próprio grupo de MCP tools). Cada módulo estende `EMCP_Tools_Module`
(`includes/modules/class-module.php`) e expõe `tier()` (`free`/`pro`), `default_active()`
(semeado uma única vez, via `EMCP_Tools_Modules_Registry::apply_defaults()`, marcador
`emcp_tools_modules_seeded`) e `is_available()` (gate adicional, ex. licença Pro).
**Um módulo `pro` pode estar "activo" no option e mesmo assim não fazer nada** — `is_available()`
exige `emcp_tools_fs()->can_use_premium_code()`; nos 3 sites verificados (build Free, sem licença),
`templates` e `agent-skills` estão semeados activos mas `is_available()` devolve `false`, logo não
correm.
| Módulo (`id`) | Tier | Activo por omissão | Disponível nesta sessão (build Free, sem licença) | O que liga quando activo+disponível |
|---|---|---|---|---|
| `prompts` | free | sim | sim | Mostra a tab de admin **Prompts** (blueprints de prompts prontos a usar para construir páginas; amostras grátis + biblioteca Pro). Não regista MCP tools — é só UI de admin (`register()` vazio). |
| `brand-kits` | free | sim | sim | Mostra a tab **Brand Kits** (kits de cor+tipografia de um clique; kits grátis + biblioteca Pro). Também só UI de admin, sem MCP tools próprias. |
| `cloud` | free | sim | sim | Arranca `EMCP_Tools_Cloud_Connect::init()` (fluxo OAuth de ligação à conta EMCP Cloud) e — só quando `EMCP_Tools_Cloud::is_connected()` for verdadeiro — regista as 7 ferramentas `cloud-*` (§5 tabela de categorias). Nos 3 sites: módulo activo mas nenhum ligado a uma conta Cloud, logo 0 tools `cloud-*` registadas. |
| `templates` | **pro** | sim (seeded) | **não** (sem licença) | Quando disponível: mostra a tab **Templates** (templates de página Elementor prontos a aplicar). Metadata-only (`register()` vazio) — vive na árvore free só como descrição, a lógica Pro real está fora deste build. |
| `themer` | free | sim | sim | O módulo mais substancial do bundle free: regista o CPT `EMCP_Tools_Themer_CPT`, o índice de condições, o controlador de render no front-end, a metabox de admin, blocos Gutenberg dinâmicos, widgets Elementor dinâmicos e templates PHP (Themer PHP, sub-toggle próprio). Desligar este módulo é kill-switch total — pára o CPT, o front-end takeover, a tab, **e** o registrador de abilities omite as 8 tools `*-theme-template*`/`*-template*` (§5). |
| `redirects` | free | sim | sim | Instala a tabela do Redirect Manager, liga o handler de 301/302 no front-end, o scan de links partidos e as sugestões de redirect ao apagar/renomear uma página. Desligar pára tudo isto **e** remove as 5 tools `*-redirect*`/`find-broken-links` do registo. |
| `agent-skills` | **pro** | sim (seeded) | **não** (sem licença) | Quando disponível: expõe `list-skills`/`get-skill` aos agentes ligados + injecta um catálogo `## Skills` (~900 tokens) no contexto de discovery. Metadata-only na árvore free. |
| `image-optimization` | free | **não** (opt-in) | depende (precisa de editor de imagem com suporte WebP) | Comprime uploads (`wp_generate_attachment_metadata`) e gera/serve WebP (rewriter de URL); regista `resize-media`. 5 opções próprias: `compress`, `webp`, `webp_serve`, `quality` (0-100, default 60), `max_dimension`, `keep_originals`. Um optimizador retomável trata a biblioteca já existente a partir do card do módulo. |
| `svg-support` | free | **não** (opt-in — superfície de segurança) | depende (precisa da lib `enshrined/svg-sanitize` bundled) | Permite upload de SVG (mime `svg`) para quem tiver a capability necessária, corrige `wp_check_filetype_and_ext()` para REST/sideload, e sanitiza cada SVG (fail-closed: um SVG que não possa ser limpo é rejeitado). Uma opção: `admin_only` (restringe upload a administradores). Nota: se o Elementor já permitir SVG (como normalmente acontece), este módulo é redundante excepto pela sanitização extra. |
---
## 7. Inventário verificado das ~162 abilities registadas (build Free, Elementor activo) — por categoria
**Correcção a uma hipótese de partida:** subtrair ingenuamente 162 − 131 = "~31 activas" está
**errado** — 84 dos 131 slugs em `emcp_tools_disabled_tools` são nomes de ferramentas **Pro**
(WooCommerce, 6 SEO plugins além do Slim SEO, 5 dos 6 plugins de formulários, frameworks de
tema além do Astra, memória do agente, Themer PHP, SEO/A11y automático, Widget/Block Builder —
ver lista completa em §2) que **nunca chegam a registar-se** neste build Free — estão pré-semeados
no option para o dia em que o site tiver licença Pro, mas `wp_get_abilities()` não os devolve porque
as classes Pro não existem na árvore free. Contá-los como "desligados" infla artificialmente a
contagem de fechadas e desinfla a de activas.
**Método de verificação usada** (a única fonte fiável — ver aviso sobre `wp mcp-adapter list` no
topo deste documento):
```bash
wp eval '
$abilities = wp_get_abilities();
$emcp = array_filter(array_keys($abilities), function($k){ return strpos($k, "emcp-tools/") === 0; });
$disabled = get_option("emcp_tools_disabled_tools", []);
$active = array_diff($emcp, $disabled);
echo "registadas=" . count($emcp) . " desligadas(option)=" . count($disabled) . " activas=" . count($active) . "\n";
' --path=$PATH --allow-root
```
**Resultado real, verificado ao vivo (16-08-2026):**
| Site | Abilities `emcp-tools/*` registadas | Entradas em `emcp_tools_disabled_tools` | … das quais correspondem a uma ability REALMENTE registada | Activas (registada E não desligada) |
|---|---|---|---|---|
| `emanuelalmeida.pt` | 162 | 131 | 47 | **115** |
| `starter.descomplicar.pt` | 160 | 131 | ~46 | **114** |
| `descomplicar.pt` | 165 | 78 | **0** | **165** (ver achado crítico, §8) |
A tabela abaixo usa `emanuelalmeida.pt` (162 registadas) como referência — é o site com a
config mais representativa da postura default-deny pretendida pelo plugin (`starter` é quase
idêntico; `descomplicar.pt` diverge de forma crítica, ver §8). Por categoria: ferramentas
**ACTIVAS** (chamáveis agora), **DESLIGADAS** (bloqueadas pelo deny-list, mas registadas — activáveis
via §4), e, quando aplicável, ferramentas do grupo que **nem chegam a registar-se** neste site
(dependem de um plugin/tema de terceiros ausente).
| Categoria | O que faz | Activas | Desligadas |
|---|---|---|---|
| **Compact tool mode (dispatcher)** | `list-tools`/`get-tool-schema`/`call-tool` — o modo de despacho compacto (3 ferramentas que expõem indirectamente todas as outras); descrito na arquitectura do servidor MCP no início deste documento. | `call-tool`, `get-tool-schema`, `list-tools` | — |
| **Elementor — descoberta/query (P0)** | Fundação de leitura: percorre containers/elementos, resolve schemas de widget/container, lista páginas Elementor e templates disponíveis. | `find-element`, `get-container-schema`, `get-element-settings`, `get-page-structure`, `get-widget-schema`, `list-pages`, `list-templates`, `list-widgets` | — |
| **Elementor — páginas (P1 CRUD)** | Cria/edita páginas Elementor, exporta o conteúdo de uma página, importa um template para uma página, e ajusta page settings (SEO básico, layout). | `create-page`, `delete-page-content`, `export-page`, `import-template`, `update-page-settings` | — |
| **Elementor — layout/container** | Manipula a árvore de containers/flexbox/elementos: adicionar, mover, duplicar, remover, reordenar, actualizar propriedades, edição em lote (`batch-update`). | `add-container`, `batch-update`, `duplicate-element`, `move-element`, `remove-element`, `reorder-elements`, `set-element-label`, `update-container`, `update-element` | — |
| **Elementor — widgets clássicos (catálogo)** | Coloca widgets Elementor gratuitos no layout e actualiza as suas definições; `add-pro-widget` (Elementor Pro) fica desligado por omissão. | `add-free-widget`, `update-widget` | `add-pro-widget` |
| **Elementor — templates/popups/dynamic tags** | `apply-template`/`save-as-template` activos (aplicar/gravar templates de página); criação de theme templates, popups, dynamic tags e condições de exibição ficam desligados (mutação estrutural). | `apply-template`, `save-as-template` | `create-elementor-theme-template`, `create-popup`, `list-dynamic-tags`, `set-dynamic-tag`, `set-elementor-template-conditions`, `set-popup-settings` |
| **Elementor — configurações globais (cores/tipografia)** | Lê e actualiza o kit global (cores e tipografia do site) — `get-global-settings` está agrupado na categoria Query (P0) mas afecta as mesmas definições. | `get-global-settings`, `update-global-colors`, `update-global-typography` | — |
| **Elementor — build-page composto** | `build-page` — ferramenta de alto nível que constrói uma página inteira a partir de uma especificação estruturada (composição de containers+widgets num único pedido). | `build-page` | — |
| **Elementor — SVG icons** | `upload-svg-icon` — envia um SVG para a biblioteca de ícones Elementor (sanitizado). | `upload-svg-icon` | — |
| **Elementor — custom code (CSS/JS/snippets)** | `add-custom-js` activo (injecta JS Elementor); `add-custom-css`, `add-code-snippet` e `list-code-snippets` ficam desligados (execução/injecção persistente). | `add-custom-js` | `add-code-snippet`, `add-custom-css`, `list-code-snippets` |
| **Elementor 4.0+ — atomic widgets** | Widgets atómicos do Elementor 4.0+ (heading, paragraph, button, image, svg, youtube, video, divider) e o genérico `add-atomic-widget`/`update-atomic-widget` — todos activos. | `add-atomic-button`, `add-atomic-divider`, `add-atomic-heading`, `add-atomic-image`, `add-atomic-paragraph`, `add-atomic-svg`, `add-atomic-video`, `add-atomic-widget`, `add-atomic-youtube`, `update-atomic-widget` | — |
| **Elementor 4.0+ — atomic layout (flexbox/div-block)** | `add-div-block`, `add-flexbox` (containers atómicos) e `detect-elementor-version` (detecta se o site suporta Elementor 4.0+ atomic). | `add-div-block`, `add-flexbox`, `detect-elementor-version` | — |
| **Elementor 4.0+ — global classes (leitura)** | `list-global-classes` — lista as classes CSS globais do Elementor 4.0+. | `list-global-classes` | — |
| **Elementor 4.0+ — global classes (escrita)** | Criar/actualizar/eliminar/reordenar classes globais — todas desligadas por omissão (mutação de CSS partilhado entre todas as páginas). | — | `create-global-class`, `delete-global-class`, `reorder-global-classes`, `update-global-class` |
| **Gutenberg (blocos nativos)** | Conjunto completo de leitura+escrita para blocos Gutenberg nativos (descoberta de blocos/patterns, inserir pattern, CRUD de blocos numa página) — todo activo por omissão (Gutenberg é considerado superfície de baixo risco face ao Elementor neste bundle). | `add-block`, `duplicate-block`, `get-block-schema`, `get-post-blocks`, `insert-pattern`, `list-blocks`, `list-patterns`, `move-block`, `remove-block`, `update-block` | — |
| **Page Snapshot** | `get-page-snapshot` — digest normalizado de uma página (base para outras ferramentas de leitura); sempre activo. | `get-page-snapshot` | — |
| **Transações/rollback (change ledger)** | `get-change`/`list-changes`/`rollback-change` — ledger de alterações feitas via MCP com possibilidade de reverter; sempre activo (é a rede de segurança das escritas). | `get-change`, `list-changes`, `rollback-change` | — |
| **Pesquisa de conteúdo (índice léxico)** | `search-content`/`reindex-search` — índice léxico sobre páginas/templates/widgets/globals. | `reindex-search`, `search-content` | — |
| **Content mirror (export/restore git-friendly)** | `export-content`/`list-content-exports`/`restore-content` — exporta conteúdo de página como ficheiros rastreáveis em git e permite restaurar. | `export-content`, `list-content-exports`, `restore-content` | — |
| **WordPress — conteúdo (posts/CPT/taxonomias)** | CRUD completo de posts/CPT (criar, ler, listar, actualizar, eliminar), listar post types/taxonomias, definir termos — activo por omissão. | `create-post`, `delete-post`, `get-post`, `list-post-types`, `list-posts`, `list-taxonomies`, `set-post-terms`, `update-post` | — |
| **WordPress — media library** | Ler/listar/actualizar/carregar media activos; `delete-media` desligado (eliminação permanente). | `get-media`, `list-media`, `update-media`, `upload-media` | `delete-media` |
| **WordPress — resize de imagem** | `resize-media` só regista quando o módulo Image Optimization está activo (reutiliza o motor de compressão/WebP) — não registado nesta sessão porque o módulo está desligado por omissão. | *(nenhuma — grupo não registado: `resize-media`)* | *(n/a — ver nota)* |
| **WordPress — settings** | `get-settings`/`update-settings` — leitura e escrita de um conjunto curado de definições do site (não é `wp_options` livre). | `get-settings`, `update-settings` | — |
| **WordPress — plugins** | Listar/pesquisar plugins fica activo; instalar/activar/desactivar/actualizar/eliminar plugin ficam desligados (superfície de maior risco depois de execução de código). | `list-plugins`, `search-plugins` | `activate-plugin`, `deactivate-plugin`, `delete-plugin`, `install-plugin`, `update-plugin` |
| **WordPress — temas (gestão)** | Listar/pesquisar temas activo; instalar/mudar/actualizar/eliminar tema desligados. | `list-themes`, `search-themes` | `delete-theme`, `install-theme`, `switch-theme`, `update-theme` |
| **WordPress — utilizadores** | Ler/listar utilizadores activo; criar/actualizar utilizador desligados (escalada de privilégios). | `get-user`, `list-users` | `create-user`, `update-user` |
| **WordPress — menus de navegação** | `menu-read`/`menu-write` — ambos activos por omissão (menus, itens, theme locations, render). | `menu-read`, `menu-write` | — |
| **Tema activo (framework-agnostic)** | `theme-read` activo (`get-theme-context`, `get-mods` — identidade do tema, theme_mods); `theme-write` desligado (`set-mods`, `create-child-theme`). | `theme-read` | `theme-write` |
| **Astra (tema)** | Só regista quando o Astra é o tema activo. `astra-read` activo (definições Astra Customizer); `astra-write` desligado. | `astra-read` | `astra-write` |
| **ACF (Advanced Custom Fields)** | Dispatcher `acf-read`/`acf-write` + 15 operações (field groups, post types, taxonomias, options pages) — só regista se ACF (free ou Pro) estiver activo no site; não registado nesta sessão (ACF ausente). | *(nenhuma — grupo não registado: `acf-read`, `acf-write`, `create-acf-field-group`, `create-acf-post-type`, `create-acf-taxonomy`, `get-acf-field-group`, `get-acf-fields`, `get-acf-post-type`, `get-acf-taxonomy`, `list-acf-field-groups`, `list-acf-options-pages`, `list-acf-post-types`, `list-acf-taxonomies`, `update-acf-field-group`, `update-acf-fields`, `update-acf-post-type`, `update-acf-taxonomy`)* | *(n/a — ver nota)* |
| **Meta Box** | Dispatcher `metabox-read`/`metabox-write` — só regista se o Meta Box estiver activo; não registado nesta sessão. | *(nenhuma — grupo não registado: `metabox-read`, `metabox-write`)* | *(n/a — ver nota)* |
| **Themer (free) — templates de header/footer/single/archive** | CPT + condições de exibição para header/footer/single/archive/search/404 com qualquer page builder. Todo o grupo activo por omissão: listar/obter/criar/actualizar/eliminar template, listar alvos de condição, definir condições, resolver qual template se aplica a um contexto. | `create-theme-template`, `delete-theme-template`, `get-theme-template`, `list-condition-targets`, `list-theme-templates`, `resolve-template`, `set-template-conditions`, `update-theme-template` | — |
| **Themer PHP templates (toggle próprio)** | Sub-funcionalidade do Themer para templates PHP brutos, atrás de um toggle independente (`EMCP_Tools_Themer_PHP::enabled()`) — não activo nesta sessão, por isso as 5 tools não estão sequer registadas. | *(nenhuma — grupo não registado: `create-theme-php-template`, `delete-theme-php-template`, `get-theme-php-template`, `list-theme-php-templates`, `update-theme-php-template`)* | *(n/a — ver nota)* |
| **Redirect Manager** | `list-redirects`/`find-broken-links` activos (leitura + scan de links partidos); `create/update/delete-redirect` desligados (mutação de routing com impacto directo em SEO). | `find-broken-links`, `list-redirects` | `create-redirect`, `delete-redirect`, `update-redirect` |
| **Stock images (busca + sideload + widget)** | `search-images`/`sideload-image`/`add-stock-image` — pesquisa num provider de stock, descarrega para a Media Library, e coloca como widget de imagem; todo activo (não muta nada fora da Media Library). | `add-stock-image`, `search-images`, `sideload-image` | — |
| **Performance Analyzer** | `analyze-performance` — auditoria de performance só de leitura. | `analyze-performance` | — |
| **Security & Malware Scanner** | `scan-security` — scanner de malware/postura de segurança, só leitura, sempre activo. | `scan-security` | — |
| **Filesystem** | `list-directory`/`read-file`/`search-files` activos; `write-file`/`edit-file`/`delete-file` desligados (escrita directa no filesystem). | `list-directory`, `read-file`, `search-files` | `delete-file`, `edit-file`, `write-file` |
| **Base de dados directa** | `describe-table`/`list-tables`/`query` (SELECT) activos; `insert-row`/`update-rows`/`delete-rows` desligados (escrita SQL directa). | `describe-table`, `list-tables`, `query` | `delete-rows`, `insert-row`, `update-rows` |
| **WP-CLI (run + jobs)** | `run-wp-cli`/`dispatch-wp-cli`/`get-wp-cli-job`/`list-wp-cli-jobs` — todo o grupo desligado por omissão (execução arbitrária de comandos WP-CLI). | — | `dispatch-wp-cli`, `get-wp-cli-job`, `list-wp-cli-jobs`, `run-wp-cli` |
| **PHP Snippets (Sandbox)** | `validate/create/update/get/list/delete-php-snippet` — todo o grupo desligado por omissão (execução de PHP arbitrário persistido). | — | `create-php-snippet`, `delete-php-snippet`, `get-php-snippet`, `list-php-snippets`, `update-php-snippet`, `validate-php-snippet` |
| **Sandbox — export/import cloud (bundle)** | `export-sandbox-artifact`/`import-sandbox-artifact` — exporta/importa um artefacto de sandbox (bloco/widget/snippet) como bundle; ambos activos (o bundle em si não executa nada, é só serialização). | `export-sandbox-artifact`, `import-sandbox-artifact` | — |
| **EMCP Cloud (sync remoto)** | `cloud-status`/`cloud-backup`/`cloud-list`/`cloud-pull`/`cloud-config-sync`/`cloud-marketplace-list`/`cloud-marketplace-install` — só registam quando o módulo Cloud está activo E o site está ligado a uma conta EMCP Cloud; não registadas nesta sessão porque nenhum dos 3 sites está ligado a uma conta Cloud (ver módulo `cloud`, §6). | *(nenhuma — grupo não registado: `cloud-backup`, `cloud-config-sync`, `cloud-list`, `cloud-marketplace-install`, `cloud-marketplace-list`, `cloud-pull`, `cloud-status`)* | *(n/a — ver nota)* |
---
## 8. Mecanismo do deny-list incremental — e um achado crítico ao vivo em `descomplicar.pt`
### 8.1 Como o plugin decide o que fecha por omissão
O deny-list não é estático: `includes/admin/class-admin.php`, método
`maybe_apply_default_disabled_tools()` (chamado no boot do admin), aplica **34 blocos de migração
incremental** (`DEFAULTS_VERSION = 34`, contador `emcp_tools_defaults_applied`), cada um
introduzido numa versão do plugin para fechar uma nova categoria de tool assim que ela é lançada
— ex.: v6 fecha escrita de plugins/temas, v9 fecha filesystem, v10 fecha base de dados, v18 fecha
WP-CLI, v28 fecha escrita de global classes, v33-34 fecham as tools de migração/sync. A função só
adiciona slugs (nunca remove uma escolha do utilizador) e só corre os blocos cujo número é maior
que o `$applied` já gravado:
```php
if ( $applied < N ) { $add = array_merge( $add, /* slugs da versão N */ ); }
// ...
update_option( self::OPTION_DISABLED_TOOLS, array_values( array_unique( array_merge( $existing, $add ) ) ) );
update_option( self::OPTION_DEFAULTS_APPLIED, (string) self::DEFAULTS_VERSION );
```
Consequência arquitectural importante: **um site cujo `emcp_tools_defaults_applied` já esteja em
34 nunca mais recebe fecho automático de tools novas**, mesmo que o plugin lance uma v35 no
futuro — nesse cenário a nova categoria nasceria **activa** nesse site até alguém a fechar à mão. E
mais relevante ainda para o presente: se o valor de `emcp_tools_disabled_tools` for alterado
manualmente (via `wp eval`/`array_diff`, como o método §4 deste documento ensina, ou por qualquer
outro caminho) **removendo** entradas em massa, o contador de versão não volta atrás — o site fica
com um deny-list mais pequeno do que o "de fábrica" para essa versão, sem qualquer aviso.
### 8.2 🔴 Achado crítico verificado ao vivo (16-08-2026) — `descomplicar.pt`
Ao aplicar o método de verificação do §7 aos 3 sites, `descomplicar.pt` destacou-se: `active=165`
= `registered=165` — **zero ferramentas efectivamente fechadas**, apesar de
`emcp_tools_disabled_tools` ter 78 entradas. Inspeccionando essas 78 entradas
(`wp option get emcp_tools_disabled_tools --format=json --path=/home/ealmeida/public_html`):
são **exclusivamente** slugs de integrações de terceiros que não estão instaladas neste site
(WooCommerce, Essential/Premium Addons, UAE, os 6 plugins de formulários, os 7 plugins de SEO,
Astra/Spectra/Kadence/GeneratePress/Blocksy, `recall`/`remember`/`save-session-summary`, os 4
achados SEO/A11y automáticos, e os 16 slugs do Widget/Block Builder Pro) — **nenhuma** das
categorias nativas mais perigosas (filesystem, base de dados, WP-CLI, PHP snippets, gestão de
plugins/temas, gestão de utilizadores) aparece nessa lista. Confirmado directamente, ferramenta a
ferramenta:
| Tool | Registada em `descomplicar.pt`? | Está no deny-list? | **Chamável agora via MCP?** |
|---|---|---|---|
| `emcp-tools/write-file` | sim | não | **🔴 SIM** |
| `emcp-tools/edit-file` | sim | não | **🔴 SIM** |
| `emcp-tools/delete-file` | sim | não | **🔴 SIM** |
| `emcp-tools/run-wp-cli` | sim | não | **🔴 SIM** |
| `emcp-tools/dispatch-wp-cli` | sim | não | **🔴 SIM** |
| `emcp-tools/insert-row` | sim | não | **🔴 SIM** |
| `emcp-tools/update-rows` | sim | não | **🔴 SIM** |
| `emcp-tools/delete-rows` | sim | não | **🔴 SIM** |
| `emcp-tools/create-php-snippet` | sim | não | **🔴 SIM** |
| `emcp-tools/update-php-snippet` | sim | não | **🔴 SIM** |
| `emcp-tools/install-plugin` | sim | não | **🔴 SIM** |
| `emcp-tools/activate-plugin` | sim | não | **🔴 SIM** |
| `emcp-tools/create-user` | sim | não | **🔴 SIM** |
| `emcp-tools/update-user` | sim | não | **🔴 SIM** |
Todas as 34 versões de migração já correram (`emcp_tools_defaults_applied = 34`, igual aos outros
2 sites), o que exclui "instalação nunca migrada" como explicação — a hipótese mais provável, dado
o padrão exacto de remoção (sobrevivem só integrações de terceiros; desaparecem só as categorias
nativas de risco máximo/alto de §5), é que o array foi reescrito manualmente nalgum momento — por
exemplo com uma variante do próprio comando de §4 aplicada de forma demasiado ampla (ex.:
`array_diff` contra uma lista de slugs "de terceiros" em vez de um único slug) — sem que a
subtracção tenha sido revertida depois.
**Isto não foi corrigido nesta tarefa** (o âmbito desta expansão de skill é só leitura — ver
constraints). **Requer decisão humana urgente**: ou (a) confirmar que a abertura destas 14+ tools
nativas em `descomplicar.pt` foi intencional e está devidamente supervisionada, ou (b) alinhar
`descomplicar.pt` com o deny-list dos outros 2 sites, repondo pelo menos as categorias 🔴🔴 de §5
(filesystem, base de dados, WP-CLI, snippets PHP) com o padrão do método §4, aplicado entrada a
entrada.
### 8.3 Regra de verificação a aplicar sempre, daqui em diante
**Nunca confiar na contagem de entradas em `emcp_tools_disabled_tools` isoladamente** — nem para
avaliar "quão fechado" está um site (v. `descomplicar.pt`, 78 entradas e 0 tools realmente
fechadas), nem para o inverso (a lista de 131 em `emanuelalmeida.pt`/`starter` também tem 84
entradas "mortas" que nunca vão fechar nada neste build). O único método correcto é o `wp eval` +
`wp_get_abilities()` do §7, comparado slug a slug contra `emcp_tools_disabled_tools` — nunca o
tamanho dos arrays, nunca a coluna `Tools` de `wp mcp-adapter list` (ver aviso na arquitectura, que
mostra `3` para `descomplicar.pt` quando o valor real é 165).
---
## Fonte
`Hub/04-Stack/02.04-Sistemas/71.Seguranca/CONFIG-Plugins-Referencia.md` §10
(EMCP Tools, mapeado 16-08-2026) + `BUNDLE-Excelencia-WP.md` (contexto do
bundle e registo dos 3 sites) + verificação SSH ao vivo nesta tarefa
(`wp option list/get`, `wp plugin get`, `wp mcp-adapter list`,
`~/.omp/agent/mcp.json`) em `descomplicar.pt`, `starter.descomplicar.pt` e
`emanuelalmeida.pt` **+ leitura completa do código-fonte do plugin
(build Free `emcp-tools` 3.12.1) em `emanuelalmeida.pt`**: todos os ficheiros
em `includes/abilities/` (46 classes de abilities + integrações
forms/seo), `includes/modules/` (9 módulos + sub-módulos Image Optimization
e SVG Support), `includes/admin/class-admin.php` (catálogo de tools,
`maybe_apply_default_disabled_tools()`, as 34 versões do deny-list
incremental), `includes/modules/class-modules-registry.php` +
`class-module.php` (ciclo de vida dos módulos), `includes/class-migration.php`
(migração `elementor-mcp` → `emcp-tools`), `emcp-tools.php` (bootstrap,
guarda Free⇄Pro) — e verificação ao vivo via `wp eval` + `wp_get_abilities()`
filtrado a `emcp-tools/*` cruzado slug-a-slug contra
`emcp_tools_disabled_tools`, nos 3 sites (método completo em §7).
@@ -0,0 +1,48 @@
---
name: powerpack-widgets
description: Catálogo completo e verificado dos 97 widgets PowerPack Elements Pro para Elementor, com o widgetType real de cada um extraído do código-fonte (classes/class-pp-config.php) — não estão no catálogo curado do EMCP Tools (list-widgets), por isso precisam de get-widget-schema com full:true. Usar quando "powerpack", "pp-widget", "advanced menu elementor", "hotspots elementor", "flip box powerpack", "woo product elementor", "info box powerpack".
layer: wiki
---
# /powerpack-widgets — Catálogo de Widgets PowerPack Elements Pro
PowerPack Elements (Team IdeaBox, plugin `powerpack-elements`) regista os próprios widgets Elementor **fora** do catálogo curado do EMCP Tools — `list-widgets` não os lista. Este catálogo foi extraído directamente do array de configuração fonte (`wp-content/plugins/powerpack-elements/classes/class-pp-config.php`, método `get_widget_info()`) num site em produção com PowerPack Elements Pro 2.11.7 — **não adivinhado por convenção de nomes**, o que importa porque a convenção `pp-<nome-módulo>` tem excepções reais confirmadas (ver notas abaixo da tabela).
Catálogo completo (97 widgets) em `references/catalogo.md` — esta página tem o workflow e os widgets mais úteis para arranque rápido.
## Como usar (workflow)
1. Confirmar que `powerpack-elements` está activo: `list-plugins`, procurar o slug.
2. `get-widget-schema({ widget_type: "pp-<slug>", full: true })` — **não estão no catálogo curado**, a chamada sem `full:true` devolve erro "Not in the curated catalog". `full:true` devolve o schema de controlos bruto.
3. Adicionar com `add-free-widget({ post_id, parent_id, widget_type, settings })` num container **legacy** (`add-container`) — mesma mecânica de `elementskit-widgets`, sem equivalente atómico.
4. **Nunca assumir o `widgetType` pelo nome da pasta do módulo** — verificado no código-fonte: `Hotspots` → `pp-image-hotspots` (não `pp-hotspots`), `Onepage_Nav` → `pp-one-page-nav` (não `pp-onepage-nav`), `Popup_Box` (pasta `modal-popup`) → `pp-modal-popup`, `Link_Effects` → **`pa-link-effects`** (prefixo `pa-`, não `pp-`!). Confirmar sempre com `get-widget-schema(full:true)` antes de construir em produção — ver tabela completa para todas as excepções.
## Widgets mais usados (ver `references/catalogo.md` para os 97 completos)
| `widgetType` | Título | Uso |
|---|---|---|
| `pp-info-box` | Info Box | ícone/imagem + título + descrição, muito usado em secções de features |
| `pp-flipbox` | Flip Box | cartão de dois lados com flip ao hover |
| `pp-advanced-menu` | Advanced Menu | menu de navegação avançado com mega-menu |
| `pp-image-hotspots` | Image Hotspots | pontos interactivos sobre imagem (excepção de nome — pasta `hotspots`) |
| `pp-pricing-table` | Pricing Table | tabela de preços |
| `pp-team-member` | Team Member | cartão de membro de equipa |
| `pp-testimonials` | Testimonials | testemunhos com carrossel |
| `pp-posts` | Advanced Posts | grelha/lista de posts avançada com query control |
| `pp-login-form` | Login Form | formulário de login frontend |
| `pp-business-hours` | Business Hours | horário de funcionamento |
| `pp-countdown` | Countdown Timer | temporizador |
| `pp-table-of-contents` | Table of Contents | índice automático |
| `pp-instafeed` | Instagram Feed | feed do Instagram |
| `pp-modal-popup` | Popup Box | popup modal (excepção de nome — pasta `modal-popup`) |
| `pp-woo-product-price` | Woo — Product Price | preço de produto WooCommerce, um de ~15 widgets `pp-woo-*` |
## Notas
- 15 dos 97 widgets são específicos WooCommerce (`pp-woo-*`) e só têm efeito útil com WooCommerce activo e no contexto de um produto/loja.
- Alguns módulos listados no directório `modules/` (`display-conditions`, `dynamic-tags`, `presets-style`, `query-control`, `devices`) são **extensões transversais**, não widgets autónomos — não aparecem no catálogo porque não têm `widgetType` próprio; afectam definições de outros widgets PowerPack (o mesmo padrão visto no `elementskit-widgets` com `ekit_cursor_text_label`).
## Skills relacionadas
- `emcp-page-building` — mecânica de coexistência atómico/legacy, sequência de construção.
- `elementor-pro-widgets` — catálogo nativo Elementor Pro (curado, no `list-widgets`).
- `elementskit-widgets` — catálogo do addon concorrente ElementsKit Lite (também fora do catálogo curado).
@@ -0,0 +1,113 @@
# Catálogo completo — 97 widgets PowerPack Elements Pro
Extraído ao vivo de `wp-content/plugins/powerpack-elements/classes/class-pp-config.php`, método `get_widget_info()`, num site em produção com PowerPack Elements Pro 2.11.7. Coluna `Classe PHP` é o identificador interno do módulo (útil para localizar o ficheiro fonte em `modules/<pasta>/widgets/`); coluna `widgetType` é o valor real gravado em `_elementor_data` e usado em `get-widget-schema`/`add-free-widget`.
| widgetType | Título | Classe PHP |
|---|---|---|
| `pp-advanced-accordion` | Advanced Accordion | Advanced_Accordion |
| `pp-advanced-menu` | Advanced Menu | Advanced_Menu |
| `pp-advanced-tabs` | Advanced Tabs | Advanced_Tabs |
| `pp-album` | Album | Album |
| `pp-author-list` | Author List | Author_List |
| `pp-breadcrumbs` | Breadcrumbs | Breadcrumbs |
| `pp-business-hours` | Business Hours | Business_Hours |
| `pp-business-reviews` | Business Reviews | Business_Reviews |
| `pp-buttons` | Buttons | Buttons |
| `pp-card-slider` | Card Slider | Card_Slider |
| `pp-categories` | Categories | Categories |
| `pp-contact-form-7` | Contact Form 7 | Contact_Form_7 |
| `pp-content-reveal` | Content Reveal | Content_Reveal |
| `pp-content-ticker` | Content Ticker | Content_Ticker |
| `pp-countdown` | Countdown Timer | Countdown |
| `pp-counter` | Counter | Counter |
| `pp-coupons` | Coupons | Coupons |
| `pp-devices` | Devices | Devices |
| `pp-divider` | Divider | Divider |
| `pp-faq` | FAQ | Faq |
| `pp-flipbox` | Flip Box | Flipbox |
| `pp-fluent-forms` | Fluent Forms | Fluent_Forms |
| `pp-formidable-forms` | Formidable Forms | Formidable_Forms |
| `pp-image-gallery` | Image Gallery | Image_Gallery |
| `pp-image-slider` | Image Slider | Image_Slider |
| `pp-google-maps` | Google Maps | Google_Maps |
| `pp-gravity-forms` | Gravity Forms | Gravity_Forms |
| `pp-dual-heading` | Dual Heading | Dual_Heading |
| `pp-fancy-heading` | Fancy Heading | Fancy_Heading |
| `pp-image-hotspots` | Image Hotspots | Hotspots |
| `pp-how-to` | How To | How_To |
| `pp-icon-list` | Icon List | Icon_List |
| `pp-image-accordion` | Image Accordion | Image_Accordion |
| `pp-image-comparison` | Image Comparison | Image_Comparison |
| `pp-info-box` | Info Box | Info_Box |
| `pp-info-box-carousel` | Info Grid & Carousel | Info_Box_Carousel |
| `pp-info-list` | Info List | Info_List |
| `pp-info-table` | Info Table | Info_Table |
| `pp-instafeed` | Instagram Feed | Instafeed |
| `pa-link-effects` | Link Effects | Link_Effects |
| `pp-login-form` | Login Form | Login_Form |
| `pp-logo-carousel` | Logo Carousel | Logo_Carousel |
| `pp-logo-grid` | Logo Grid | Logo_Grid |
| `pp-magazine-slider` | Magazine Slider | Magazine_Slider |
| `pp-ninja-forms` | Ninja Forms | Ninja_Forms |
| `pp-offcanvas-content` | Offcanvas Content | Offcanvas_Content |
| `pp-one-page-nav` | One Page Navigation | Onepage_Nav |
| `pp-modal-popup` | Popup Box | Popup_Box |
| `pp-posts` | Advanced Posts | Posts |
| `pp-price-menu` | Price Menu | Price_Menu |
| `pp-pricing-table` | Pricing Table | Pricing_Table |
| `pp-promo-box` | Promo Box | Promo_Box |
| `pp-random-image` | Random Image | Random_Image |
| `pp-recipe` | Recipe | Recipe |
| `pp-registration-form` | Registration Form | Registration_Form |
| `pp-review-box` | Review Box | Review_Box |
| `pp-scroll-image` | Scroll Image | Scroll_Image |
| `pp-showcase` | Showcase | Showcase |
| `pp-sitemap` | Sitemap | Sitemap |
| `pp-table` | Table | Table |
| `pp-tabbed-gallery` | Tabbed Gallery | Tabbed_Gallery |
| `pp-table-of-contents` | Table of Contents | Table_Of_Contents |
| `pp-team-member` | Team Member | Team_Member |
| `pp-team-member-carousel` | Team Member Carousel | Team_Member_Carousel |
| `pp-testimonials` | Testimonials | Testimonials |
| `pp-tiled-posts` | Tiled Posts | Tiled_Posts |
| `pp-timeline` | Timeline | Timeline |
| `pp-toggle` | Toggle | Toggle |
| `pp-twitter-buttons` | Twitter Buttons | Twitter_Buttons |
| `pp-twitter-grid` | Twitter Grid | Twitter_Grid |
| `pp-twitter-timeline` | Twitter Timeline | Twitter_Timeline |
| `pp-twitter-tweet` | Twitter Tweet | Twitter_Tweet |
| `pp-video` | Video | Video |
| `pp-video-gallery` | Video Gallery | Video_Gallery |
| `pp-wpforms` | WP Forms | WP_Forms |
| `pp-woo-add-to-cart` | Woo — Add To Cart | Woo_Add_To_Cart |
| `pp-woo-cart` | Woo — Cart | Woo_Cart |
| `pp-woo-categories` | Woo — Categories | Woo_Categories |
| `pp-woo-checkout` | Woo — Checkout | Woo_Checkout |
| `pp-woo-mini-cart` | Woo — Mini Cart | Woo_Mini_Cart |
| `pp-woo-offcanvas-cart` | Woo — Off Canvas Cart | Woo_Offcanvas_Cart |
| `pp-woo-products` | Woo — Products | Woo_Products |
| `pp-woo-my-account` | Woo — My Account | Woo_My_Account |
| `pp-woo-product-tabs` | Woo — Product Tabs | Woo_Product_Tabs |
| `pp-woo-product-title` | Woo — Product Title | Woo_Product_Title |
| `pp-woo-product-meta` | Woo — Product Meta | Woo_Product_Meta |
| `pp-woo-product-price` | Woo — Product Price | Woo_Product_Price |
| `pp-woo-product-rating` | Woo — Product Rating | Woo_Product_Rating |
| `pp-woo-product-stock` | Woo — Product Stock | Woo_Product_Stock |
| `pp-woo-product-short-description` | Woo — Product Short Description | Woo_Product_Short_Description |
| `pp-woo-product-content` | Woo — Product Content | Woo_Product_Content |
| `pp-woo-product-images` | Woo — Product Images | Woo_Product_Images |
| `pp-woo-product-reviews` | Woo — Product Reviews | Woo_Product_Reviews |
| `pp-woo-product-upsell` | Woo — Product Upsell | Woo_Product_Upsell |
| `pp-woo-add-to-cart-notification` | Woo — Add to Cart Notification | Woo_Add_To_Cart_Notification |
| `pp-woo-archive-description` | Woo — Archive Description | Woo_Archive_Description |
| `pp-woo-single-product` | Woo — Single Product | Woo_Single_Product |
## Excepções confirmadas ao nome-de-pasta óbvio
- `Hotspots` (pasta `modules/hotspots`) → `pp-image-hotspots`, não `pp-hotspots`.
- `Onepage_Nav` (pasta `modules/onepage-nav`) → `pp-one-page-nav`, não `pp-onepage-nav`.
- `Popup_Box` (pasta `modules/modal-popup`) → `pp-modal-popup`.
- `Link_Effects` (pasta `modules/link-effects`) → **`pa-link-effects`** — único widget com prefixo `pa-` em vez de `pp-` em todo o catálogo.
- `WP_Forms` → `pp-wpforms` (sem hífen entre "wp" e "forms").
Regra geral: confirmar sempre `widgetType` com `get-widget-schema({ widget_type, full: true })` antes de o usar em `add-free-widget` — este ficheiro é a fonte mais fiável disponível, mas widgets podem ser adicionados/renomeados em versões futuras do plugin.
+105 -4
View File
@@ -1,6 +1,6 @@
---
name: rank-math
description: Gestão programática do Rank Math SEO (Free e PRO) via WP-CLI em servidores CWP. Cobre auditoria SEO, meta em massa, schema markup, redirects, sitemaps, Instant Indexing, Analytics PRO, hooks/filtros, taxonomias WooCommerce, News/Video Sitemap, validação e rollback. Usar quando "rank math", "seo cli", "schema markup", "redirects seo", "instant indexing", "indexnow", "seo bulk", "rankmath pro".
description: Gestão programática do Rank Math SEO (Free e PRO) via WP-CLI em servidores CWP. Cobre auditoria SEO, meta em massa, schema markup, redirects, sitemaps, Instant Indexing, Analytics PRO, hooks/filtros, taxonomias WooCommerce, News/Video Sitemap, módulos Free vs PRO completos (image-seo, local-seo, 404-monitor, llms-txt, news-sitemap, marketplace, ai-visibility), validação e rollback. Licenciamento/ligação de conta PRO → ver `/full-cliente`. Usar quando "rank math", "seo cli", "schema markup", "redirects seo", "instant indexing", "indexnow", "seo bulk", "rankmath pro", "llms.txt", "módulos rank math".
---
# /rank-math — Gestão Programática Rank Math SEO via WP-CLI
@@ -1016,6 +1016,59 @@ wp post meta get ID rank_math_robots --format=json --allow-root --path=$PATH
---
## 15. Módulos — referência completa (Free vs PRO)
Lista verificada ao vivo em `descomplicar.pt` (Rank Math Pro activo, licença "full services") via `wp option get rank_math_modules --format=json` e confirmada contra o filesystem (`includes/modules/` de `seo-by-rank-math` e `seo-by-rank-math-pro`), 16-08-2026.
```bash
# Ver modulos activos (array INDEXADO de slugs — NAO um map slug=>bool)
wp option get rank_math_modules --format=json --allow-root --path=$PATH
# Activar um modulo (wp eval — NUNCA wp option patch update aqui, a option e um array sequencial)
wp eval '
$modules = get_option("rank_math_modules", []);
if (!in_array("SLUG", $modules)) { $modules[] = "SLUG"; update_option("rank_math_modules", $modules); }
' --allow-root --path=$PATH
# Desactivar um modulo
wp eval '
$modules = get_option("rank_math_modules", []);
$modules = array_values(array_diff($modules, ["SLUG"]));
update_option("rank_math_modules", $modules);
' --allow-root --path=$PATH
```
| Slug | Nome | Disponibilidade | Nota |
|------|------|------------------|------|
| `seo-analysis` | SEO Analysis | Free | Score SEO no editor |
| `link-counter` (dir `links`) | Link Counter | Free | Contagem links internos/externos |
| `sitemap` | XML Sitemap | Free | — |
| `rich-snippet` (dir `schema`) | Schema/Rich Snippets | Free | 1 schema/pagina; PRO liberta multiplos + Schema Builder custom |
| `redirections` | Redirections | Free (Advanced Mode) | — |
| `404-monitor` | 404 Monitor | Free (opcao basica) / **PRO (modulo dedicado)** | Free: so `404_monitor_mode` em General, sem tabela propria. PRO: modulo com tabela `wp_rank_math_404_logs` |
| `instant-indexing` | Instant Indexing (IndexNow) | Free | Google Indexing API adicional (via classes RM) e PRO |
| `woocommerce` | WooCommerce SEO | Free | Requer plugin WooCommerce activo |
| `acf` | ACF Integration | Free | Requer plugin ACF activo |
| `analytics` | Analytics | Free (basico) / PRO (avancado) | GSC/GA; historico alargado e PRO |
| `role-manager` | Role Manager | Free | — |
| `buddypress` | BuddyPress Integration | Free | Requer plugin BuddyPress |
| `bbpress` (dir `bbPress`) | bbPress Integration | Free | Requer plugin bbPress |
| `web-stories` | Web Stories SEO | Free | Requer plugin Web Stories |
| `ai-visibility` | AI Visibility | Free | Achado 16-08-2026 — modulo novo, monitoriza citacoes em AI Overviews/LLMs |
| `content-ai` | Content AI | Free (limitado, requer conta ligada) / PRO (ilimitado) | Confirmado activo em `emanuelalmeida.pt` mesmo antes do Pro — depende da conta ligada, nao do plugin Pro |
| `image-seo` | Image SEO | **PRO exclusivo** | Alt/title automatico em imagens |
| `local-seo` | Local SEO | **PRO exclusivo** | Schema LocalBusiness multi-localizacao |
| `llms-txt` (dir `llms`) | LLMs.txt | **PRO exclusivo** | Gera `/llms.txt` automaticamente — ver achado de sessao em §16.2 |
| `news-sitemap` | News Sitemap | **PRO exclusivo** | — |
| `video-sitemap` | Video Sitemap | **PRO exclusivo** | Confirmado no filesystem (`includes/modules/video-sitemap`), nao estava activo no site verificado |
| `marketplace` | Marketplace (addons) | **PRO exclusivo** | — |
| `link-genius` | Link Genius | **PRO exclusivo** | So existe no filesystem Pro; sem equivalente Free |
| `podcast` | Podcast SEO | **PRO exclusivo** | So existe no filesystem Pro |
**Nota sobre `image-seo` e `local-seo`:** existe a pasta do modulo em AMBOS os plugins (`seo-by-rank-math/includes/modules/image-seo` e `local-seo`), mas o registo efectivo do modulo como disponivel para activacao depende da licenca PRO ligada — confirmado ao vivo: so aparecem em `rank_math_modules` em sites com Pro activo e licenciado.
---
## Sequencia de cache (sempre apos alteracoes)
```bash
@@ -1081,7 +1134,8 @@ wp rankmath sitemap generate --allow-root --path=$PATH
| Schemas multiplos/post | nao | sim |
| Custom Schema Builder (840+ tipos) | nao | sim |
| Redirections | sim | sim |
| 404 Monitor | sim | sim |
| 404 Monitor (opção básica) | sim | sim |
| 404 Monitor (módulo dedicado, tabela própria — ver §15) | nao | sim |
| Instant Indexing (IndexNow) | sim | sim |
| Google Indexing API | nao | sim |
| Analytics GSC/GA | basico | avancado |
@@ -1126,6 +1180,53 @@ wp cache flush --allow-root --path=$PATH
---
## 16. Achados de sessão — bundle Descomplicar (16-08-2026)
Achados da auditoria de seguranca/infra ao bundle de 7 sites Descomplicar (`CONFIG-Plugins-Referencia.md` §7, `BUNDLE-Excelencia-WP.md`). Licenca Rank Math confirmada pelo utilizador como **"full services"** — cobre os 7 sites, nao e por assento individual.
### 16.1 Distribuicao real do Pro nos 7 sites
| Site | `seo-by-rank-math-pro` instalado | Licenca ligada (`rank_math_connect_data.connected`) | Estado |
|------|-----------------------------------|-------------------------------------------------------|--------|
| `descomplicar.pt` | sim | sim | Pro operacional |
| `carstuff.pt` | sim | sim | Pro operacional |
| `emanuelalmeida.pt` | sim | sim | Pro operacional |
| `familyclinic.pt` | sim | sim | Pro operacional |
| `ignitionvortex.pt` | sim | nao | Plugin instalado, **licenca por ligar** |
| `solarfvengenharia.com` | sim | nao | Plugin instalado, **licenca por ligar** |
| `watercontrol.pt` | sim | nao | Plugin instalado, **licenca por ligar** |
A ligacao de conta faz-se por OAuth no wp-admin (Rank Math → Ligar a sua conta). **Nao simular via wp-cli** copiando `rank_math_connect_data` de outro dominio — a API do Rank Math valida `site_url` no lado do servidor; copiar o valor arrisca invalidar ou confundir a licenca de origem.
```bash
# Verificar ligacao de conta num site
wp option get rank_math_connect_data --format=json --allow-root --path=$PATH
```
### 16.2 `llms-txt` como causa raiz de `/llms.txt` em falta
`/llms.txt` esteve em falta em `emanuelalmeida.pt` (desde 12-08-2026 pelo menos) apesar da conta Rank Math ja estar ligada. Causa raiz: o modulo `llms-txt` (gera `/llms.txt` automaticamente) e **exclusivo Pro** — o site so tinha `seo-by-rank-math` (free) instalado, sem `seo-by-rank-math-pro`. Corrigido 16-08-2026: instalado o plugin Pro + activado o modulo `llms-txt` (nao vem activo por omissao mesmo com Pro) + verificado `/llms.txt` → HTTP 200 ao vivo, em `emanuelalmeida.pt` e `familyclinic.pt`.
```bash
# Diagnosticar /llms.txt em falta
wp option get rank_math_modules --format=json --allow-root --path=$PATH | grep -q llms-txt && echo "modulo activo" || echo "modulo AUSENTE — instalar Pro + activar"
# Activar o modulo depois do Pro estar instalado e licenciado
wp eval '
$modules = get_option("rank_math_modules", []);
if (!in_array("llms-txt", $modules)) { $modules[] = "llms-txt"; update_option("rank_math_modules", $modules); }
' --allow-root --path=$PATH
# Verificar ao vivo
curl -sI "https://SITE.pt/llms.txt" | head -1
```
### 16.3 Licencas — usar `/full-cliente`, nao duplicar aqui
A gestao de licenca/ligacao de conta Rank Math Pro faz parte do broker de licencas **FULL.Cliente** (`api.full.services`), que suporta `rankMath` como product key numa chamada unica (sem steps de OTP, ao contrario de `essentialAddons`). Para ligar/reactivar a licenca Rank Math Pro em qualquer site do bundle, usar a skill `/full-cliente` em vez de replicar aqui os comandos do broker — esta skill (`/rank-math`) fica focada em WP-CLI/dados do plugin ja licenciado.
---
## Recursos
### Referencias
@@ -1140,9 +1241,9 @@ wp cache flush --allow-root --path=$PATH
- **Pesquisa Gemini:** `Hub/06-Operacoes/Documentacao/Manuais/WP-CLI/Rank-Math-SEO-WP-CLI-Skills-Pesquisa-Gemini.md`
- **NotebookLM:** [WordPress Config CLI](https://notebooklm.google.com/notebook/fb2f26bd-8cb0-4d4c-bafc-4f1ebb51c51d) (4 fontes)
---
**Fonte:** manual completo `Rank-Math-WP-CLI-Manual-Definitivo.md` (17 seccoes, lido na integra), codigo-fonte `includes/modules/` de `seo-by-rank-math` e `seo-by-rank-math-pro` lido ao vivo via SSH em `descomplicar.pt`, `wp option get rank_math_modules` verificado em produção, achados da auditoria de seguranca/infra 16-08-2026 (`CONFIG-Plugins-Referencia.md` §7, `BUNDLE-Excelencia-WP.md`).
*Rank Math SEO via WP-CLI | Descomplicar | v2.0.0 | 29-03-2026*
*Rank Math SEO via WP-CLI | Descomplicar | v2.1.0 | 16-08-2026*
---
@@ -137,28 +137,52 @@ wp option pluck rank-math-options-sitemap pt_attachment_sitemap # on/off (defau
## Módulos Rank Math
`rank_math_modules` e um **array indexado de slugs activos** (nao um map `slug=>bool`). Lista completa verificada ao vivo em `descomplicar.pt` (Pro) + filesystem — ver `SKILL.md` §15 para a tabela completa Free vs PRO.
```bash
# Ver todos os módulos
# Ver todos os módulos activos
wp option get rank_math_modules --format=json --allow-root
# Activar módulo
wp option patch update rank_math_modules MODULE_NAME "1" --allow-root
# Activar módulo (wp eval — wp option patch NAO funciona aqui, nao ha chave por slug)
wp eval '
$modules = get_option("rank_math_modules", []);
if (!in_array("MODULE_SLUG", $modules)) { $modules[] = "MODULE_SLUG"; update_option("rank_math_modules", $modules); }
' --allow-root
# Desactivar módulo
wp option patch update rank_math_modules MODULE_NAME "0" --allow-root
wp eval '
$modules = get_option("rank_math_modules", []);
$modules = array_values(array_diff($modules, ["MODULE_SLUG"]));
update_option("rank_math_modules", $modules);
' --allow-root
```
| Módulo | Slug | Função |
|--------|------|--------|
| SEO Analysis | `seo-analysis` | Score SEO no editor |
| Link Counter | `link-counter` | Contagem links internos/externos |
| Redirections | `redirections` | Sistema de redirects |
| 404 Monitor | `404-monitor` | Log de 404s |
| Schema | `rich-snippet` | Schema markup |
| Sitemap | `sitemap` | XML Sitemap |
| Instant Indexing | `instant-indexing` | Google Indexing API (PRO) |
| Local SEO | `local-seo` | Schema LocalBusiness |
| WooCommerce | `woocommerce` | SEO WooCommerce |
| Módulo | Slug | Função | Tier |
|--------|------|--------|------|
| SEO Analysis | `seo-analysis` | Score SEO no editor | Free |
| Link Counter | `link-counter` | Contagem links internos/externos | Free |
| Redirections | `redirections` | Sistema de redirects | Free |
| 404 Monitor (dedicado, tabela própria) | `404-monitor` | Log de 404s | Free (opção básica) / PRO (módulo completo) |
| Schema | `rich-snippet` | Schema markup | Free (1/página) / PRO (ilimitado + Builder) |
| Sitemap | `sitemap` | XML Sitemap | Free |
| Instant Indexing | `instant-indexing` | IndexNow; Google Indexing API é PRO | Free |
| WooCommerce | `woocommerce` | SEO WooCommerce | Free |
| ACF | `acf` | Integração Advanced Custom Fields | Free |
| Analytics | `analytics` | GSC/GA no wp-admin | Free (básico) / PRO (avançado) |
| Role Manager | `role-manager` | Capabilities por role | Free |
| BuddyPress | `buddypress` | Integração BuddyPress | Free |
| bbPress | `bbpress` | Integração bbPress | Free |
| Web Stories | `web-stories` | SEO para Web Stories | Free |
| AI Visibility | `ai-visibility` | Monitoriza citações em AI Overviews/LLMs (achado 16-08-2026) | Free |
| Content AI | `content-ai` | Assistente de conteúdo (requer conta ligada) | Free (limitado) / PRO (ilimitado) |
| Image SEO | `image-seo` | Alt/title automático em imagens | **PRO exclusivo** |
| Local SEO | `local-seo` | Schema LocalBusiness multi-localização | **PRO exclusivo** |
| LLMs.txt | `llms-txt` | Gera `/llms.txt` automaticamente | **PRO exclusivo** |
| News Sitemap | `news-sitemap` | Sitemap Google News | **PRO exclusivo** |
| Video Sitemap | `video-sitemap` | Sitemap de vídeos | **PRO exclusivo** |
| Marketplace | `marketplace` | Addons Rank Math | **PRO exclusivo** |
| Link Genius | `link-genius` | Sugestões de link interno automáticas | **PRO exclusivo** |
| Podcast | `podcast` | SEO para podcasts | **PRO exclusivo** |
---
@@ -367,13 +391,20 @@ wp db query "SELECT id, sources, url_to, header_code FROM wp_rank_math_redirecti
| `wp rankmath sitemap generate` | ✅ | ✅ | Único comando nativo |
| Meta por post | ✅ | ✅ | `wp post meta` |
| Options globais | ✅ | ✅ | `wp option patch` |
| Schema markup | ✅ | ✅ | `wp eval` |
| Schema markup | ✅ (1/página) | ✅ (ilimitado + Schema Builder) | `wp eval` |
| Redirections | ✅ | ✅ | `\RankMath\Redirections\DB` |
| 404 Monitor | ✅ | ✅ | `wp_rank_math_404_logs` |
| Instant Indexing | ❌ | ✅ | `\RankMath\Instant_Indexing\Api` |
| Analytics GSC | ❌ | ✅ | Tabelas analytics_gsc |
| Analytics GA | ❌ | ✅ | Tabelas analytics_ga |
| 404 Monitor (opção básica, `404_monitor_mode`) | ✅ | ✅ | Sem tabela própria em Free |
| 404 Monitor (módulo dedicado, `wp_rank_math_404_logs`) | ❌ | ✅ | Verificado ao vivo 16-08-2026 — só existe com Pro licenciado |
| Instant Indexing — IndexNow | ✅ | ✅ | Módulo `instant-indexing`, ambos free e PRO |
| Instant Indexing — Google Indexing API | ❌ | ✅ | `\RankMath\Instant_Indexing\Api` |
| Analytics (básico, wp-admin) | ✅ | ✅ | Módulo `analytics` presente em ambos |
| Analytics GSC/GA (histórico alargado) | ❌ | ✅ | Tabelas `analytics_gsc`/`analytics_ga` |
| Image SEO | ❌ | ✅ | Módulo `image-seo` — confirmado PRO exclusivo |
| Local SEO | ❌ | ✅ | Módulo `local-seo` — confirmado PRO exclusivo |
| LLMs.txt (`/llms.txt` automático) | ❌ | ✅ | Módulo `llms-txt` — confirmado PRO exclusivo (achado 16-08-2026) |
| News/Video Sitemap | ❌ | ✅ | Módulos `news-sitemap`/`video-sitemap` |
| Marketplace / Link Genius / Podcast | ❌ | ✅ | Só existem no filesystem `seo-by-rank-math-pro` |
---
*Referência Rank Math WP-CLI | Descomplicar® | v1.0.0 | 18-02-2026*
*Referência Rank Math WP-CLI | Descomplicar® | v1.1.0 | 16-08-2026*
@@ -0,0 +1,460 @@
---
name: redis-object-cache
description: Gestão e diagnóstico do plugin Redis Object Cache (redis-cache, Till Krüss) via WP-CLI em servidores CWP. Cobre estado da ligação (wp redis status), drop-in object-cache.php, isolamento por site via WP_REDIS_DATABASE/WP_REDIS_PREFIX no bundle partilhado, PING/PONG, flush pós-SQL directo, e diferença entre object cache e page cache (WP Fastest Cache/WP Meteor). Usar quando "redis object cache", "redis-cache", "wp redis status", "object cache", "isolamento redis", "colisão de base redis", "WP_REDIS_DATABASE", "cache não invalida", "predis vs phpredis".
---
# /redis-object-cache — Gestão e Diagnóstico Redis Object Cache
Plugin `redis-cache` (Till Krüss), versão `2.8.0` confirmada em produção.
Object cache persistente — cache de objectos/queries WordPress, **não** é
page cache (ver secção "Object cache vs page cache" abaixo).
**Fonte:** `CONFIG-Plugins-Referencia.md` secção 6 (Redis Object Cache,
mapeado 16-08-2026) + verificação SSH ao vivo desta sessão (16-08-2026) em
`emanuelalmeida.pt` e nos 7 sites do bundle + expansão de cobertura
(16-08-2026) por leitura integral do código-fonte do plugin
(`includes/object-cache.php`, `includes/class-plugin.php`,
`includes/class-predis.php`, `includes/class-metrics.php`,
`includes/diagnostics.php`, `includes/cli/class-commands.php`,
`wp-includes/load.php` do core) — mapeamento exaustivo de todas as
constantes `WP_REDIS_*`, subcomandos WP-CLI, hooks e semântica de grupos,
não apenas o subconjunto tocado na auditoria pontual original.
---
## Contexto CWP — sempre obrigatório
```bash
# Via SSH directo (mais rápido para wp redis, comando nativo)
ssh server "sudo -u USER /usr/local/bin/wp redis status --path=/home/USER/SITE_PATH"
# Formato CWP completo (PHP versionado)
sudo -u USER /opt/alt/php-fpm82/usr/bin/php /usr/local/bin/wp redis status \
--allow-root --path=/home/USER/public_html
```
```
servidor: server.descomplicar.pt | porta: 9443 | user: root
```
**Único comando nativo do plugin:** `wp redis <subcomando>`. Confirmado ao
vivo (`wp redis --help --path=$PATH`):
| Subcomando | Faz |
|---|---|
| `wp redis status` | Mostra ligação, client, host/porta/DB/prefixo, grupos, drop-in |
| `wp redis enable` | Activa a cache (escreve o drop-in `object-cache.php`) |
| `wp redis disable` | Desactiva a cache (remove o drop-in) |
| `wp redis update-dropin` | Actualiza o drop-in para a versão do plugin instalado |
**Gotcha real confirmado nesta sessão:** `wp redis flush` **não existe**.
A própria ajuda do comando diz: *"To flush call `wp cache flush`"*. Usar
sempre o comando genérico do WP-CLI (`wp cache flush`), que funciona porque
o drop-in `object-cache.php` está activo e intercepta o wrapper nativo.
**Reconfirmado nesta expansão (16-08-2026):** `wp help redis --path=$PATH`
ao vivo devolve exactamente os mesmos 4 subcomandos. O código-fonte
(`includes/cli/class-commands.php`, classe `Commands extends WP_CLI_Command`)
só define 4 métodos públicos — `status()`, `enable()`, `disable()`,
`update_dropin()` (mapeado para `update-dropin` via `@subcommand`) — pelo que
esta lista é exaustiva, não apenas a amostra testada. Não existe `wp redis
flush` nem `wp redis info` nesta versão (2.8.0).
---
## 1. Diagnóstico rápido — `wp redis status`
```bash
PATH=/home/USER/SITE_PATH
wp redis status --path=$PATH
```
Output real (`emanuelalmeida.pt`, DB 12):
```
Status: Ligado
Client: Predis (v2.4.0)
Drop-in: Valid
Disabled: No
Ping: PONG
Errors: []
PhpRedis: Not loaded
Relay: Not loaded
Predis: 2.4.0
PHP Version: 8.2.31
Plugin Version: 2.8.0
Redis Version: 5.0.3
WP_REDIS_HOST: "127.0.0.1"
WP_REDIS_PORT: 6379
WP_REDIS_DATABASE: 12
WP_REDIS_PREFIX: "emanuelalmeida_pt_"
WP_CACHE_KEY_SALT: "..."
Timeout: 1
Read Timeout: 1
Global Groups: [...24 grupos: blog-details, users, site-transient, ...]
Ignored Groups: ["counts", "plugins", "theme_json", "WPForms_Entry_Handler", "themes"]
Drop-ins: ["Redis Object Cache Drop-In v2.8.0 by Till Krüss"]
```
| Campo a verificar | Valor esperado | Se falhar |
|---|---|---|
| `Status` | `Ligado` | `Não ligado` → drop-in ausente ou desactivado, correr `wp redis enable` |
| `Ping` | `PONG` | Qualquer outra coisa → Redis não responde na porta 6379, verificar serviço `redis-server` no host |
| `Drop-in` | `Valid` | `Invalid`/`Outdated` → correr `wp redis update-dropin` |
| `Client` | `Predis 2.4.0` | Confirmado — `PhpRedis`/`Relay` **não instalados** no servidor (extensão PHP nativa ausente); Predis é fallback 100% funcional em PHP puro, apenas mais lento. Não é erro, é a configuração real e estável do servidor |
| `WP_REDIS_DATABASE` | número único por site | Ver secção "Isolamento entre sites" |
Ler `object-cache.php` como drop-in activo (não apenas plugin) é o
diferencial: `wp plugin is-active redis-cache` só confirma que o plugin
está ligado, **não** confirma que o drop-in está a interceptar as chamadas
de cache. `wp redis status` é a única fonte fiável de estado real.
---
## 2. Isolamento entre sites do bundle — verificação de integridade
Todos os sites do bundle Descomplicar® partilham a **mesma instância Redis**
(`127.0.0.1:6379`, um único servidor Redis no host). O isolamento é feito por
duas camadas independentes — `WP_REDIS_DATABASE` (índice de base lógica
Redis, 0-15 por omissão) **e** `WP_REDIS_PREFIX` (prefixo de chave) — para
que uma colisão numa camada não cause colisão real de dados.
### Comando repetível — listar DB de todos os sites e confirmar zero colisão
```bash
#!/usr/bin/env bash
# Corre no servidor (via ssh server "...") ou local com ssh -p 9443 root@server.descomplicar.pt
declare -A SITES=(
[descomplicar.pt]="ealmeida:/home/ealmeida/public_html"
[emanuelalmeida.pt]="ealmeida:/home/ealmeida/emanuelalmeida.pt"
[carstuff.pt]="carstuff:/home/carstuff/public_html"
[familyclinic.pt]="familycl:/home/familycl/public_html"
[ignitionvortex.pt]="ignition:/home/ignition/public_html"
[solarfvengenharia.com]="solarfv:/home/solarfv/public_html"
[watercontrol.pt]="wtc:/home/wtc/public_html"
)
DBS=""
for site in "${!SITES[@]}"; do
user="${SITES[$site]%%:*}"; path="${SITES[$site]#*:}"
active=$(sudo -u "$user" /usr/local/bin/wp plugin is-active redis-cache --path="$path" 2>/dev/null && echo ACTIVE || echo INACTIVE)
db=$(sudo -u "$user" /usr/local/bin/wp config get WP_REDIS_DATABASE --path="$path" 2>/dev/null)
prefix=$(sudo -u "$user" /usr/local/bin/wp config get WP_REDIS_PREFIX --path="$path" 2>/dev/null)
printf "%-24s %-9s DB=%-3s prefix=%s\n" "$site" "$active" "$db" "$prefix"
[ "$active" = "ACTIVE" ] && DBS="$DBS $db"
done
echo "--- DBs activas: $DBS"
echo "--- duplicados:"; echo $DBS | tr ' ' '\n' | sort | uniq -d
```
**Resultado real desta sessão (16-08-2026):**
| Site | Plugin | `WP_REDIS_DATABASE` | `WP_REDIS_PREFIX` |
|---|---|---|---|
| descomplicar.pt | **inactive** | `0` (default, não usado) | — |
| emanuelalmeida.pt | active | `12` | `emanuelalmeida_pt_` |
| carstuff.pt | active | `1` | `carstuff_` |
| familyclinic.pt | active | `3` | `familycl_` |
| ignitionvortex.pt | active | `4` | `ignition_` |
| solarfvengenharia.com | active | `6` | `solarfv_` |
| watercontrol.pt | active | `7` | `wtc_` |
**Zero colisões** — DBs activas `{1, 3, 4, 6, 7, 12}` são todas distintas, e
mesmo que duas colidissem, o `WP_REDIS_PREFIX` distinto por site seria a
segunda barreira. `descomplicar.pt` (site principal Descomplicar®) tem o
plugin **instalado mas inactivo** — não usa object cache Redis; confirmar
antes de assumir que está coberto pelo bundle.
**Gotcha:** a linha `echo $DBS | tr ' ' '\n' | sort | uniq -d` fica vazia
quando não há duplicados — silêncio = sucesso. Um output não-vazio ali é
alarme real de colisão de DB entre dois sites do bundle.
---
## 3. Object cache vs page cache — não conflitam, são camadas diferentes
| Camada | Plugin | O que guarda | Onde vive |
|---|---|---|---|
| **Object cache** | Redis Object Cache (este) | Resultados de queries SQL, `wp_options` autoload, transients, meta — substitui o cache-in-memory por pedido do WordPress (`WP_Object_Cache`) por um persistente entre pedidos | Redis (`127.0.0.1:6379`) |
| **Page cache** | WP Fastest Cache / WP Meteor | HTML final já renderizado de uma página, servido antes do WordPress arrancar | Ficheiros estáticos em disco (`wp-content/cache/`) |
São independentes e **complementares**: o page cache evita mesmo carregar o
WordPress; quando falha um cache de página (visitante logado, POST,
página não cacheável) é o object cache Redis que absorve o custo das
queries repetidas. Não há necessidade de coordenar TTLs ou invalidação
entre os dois — cada um limpa-se pelo seu próprio mecanismo (`wp cache
flush` para Redis; purga própria do WP Fastest Cache/WP Meteor para HTML).
---
## 4. Gotcha crítico — SQL directo não invalida o Redis
**Mesma nota da skill `/rank-math`:** qualquer alteração feita via `wp db
query` (UPDATE/DELETE directo em `wp_options`, `wp_postmeta`, etc.) **não
passa pelas funções WordPress** (`update_option()`, `update_post_meta()`)
que disparam a invalidação de cache. Com o object cache Redis activo, os
dados antigos continuam servidos da cache até expirarem ou até um flush
explícito — o que pode demorar minutos a horas dependendo do grupo.
**Regra:** SEMPRE `wp cache flush --path=$PATH` imediatamente a seguir a
qualquer `wp db query` de escrita, antes de validar o resultado no site.
```bash
wp db query "UPDATE ${PREFIX}options SET option_value='...' WHERE option_name='...';" --path=$PATH
wp cache flush --path=$PATH # OBRIGATÓRIO — sem isto o Redis serve dados stale
```
Sem este passo, uma auditoria/correcção pode parecer ter falhado (o browser
mostra o valor antigo) quando na verdade a escrita na BD foi bem-sucedida —
é só a camada de cache que ainda não sabe.
---
## 5. Grupos ignorados por omissão
Confirmado via `wp redis status`:
```
Ignored Groups: ["counts", "plugins", "theme_json", "WPForms_Entry_Handler", "themes"]
```
Estes grupos ficam **fora** do Redis mesmo com o object cache activo —
mudam com frequência ou têm de reflectir estado imediato (contagem de
comentários, lista de plugins/temas). É a configuração por omissão do
plugin, correcta para não introduzir dados desactualizados nestes pontos.
Os restantes 24 grupos (`users`, `site-transient`, `usermeta`,
`redis-cache`, etc.) são cacheados normalmente — ver output completo de
`wp redis status` para a lista exaustiva por site.
---
## 6. Constantes completas suportadas (`WP_REDIS_*`)
Levantamento exaustivo por leitura integral de
`includes/object-cache.php` (drop-in, 3066 linhas), `includes/class-plugin.php`
(1685 linhas), `includes/class-predis.php`, `includes/class-metrics.php` e
`includes/diagnostics.php` — não apenas as 5 constantes usadas no bundle
Descomplicar®. Todas por definir em `wp-config.php` **antes** do WordPress
arrancar (mesmo princípio do `WP_REDIS_DATABASE`/`WP_REDIS_PREFIX` já usado).
### Ligação básica
| Constante | Default | Descrição |
|---|---|---|
| `WP_REDIS_HOST` | `127.0.0.1` | Host do servidor Redis |
| `WP_REDIS_PORT` | `6379` | Porta TCP |
| `WP_REDIS_PATH` | — | Caminho do socket Unix; só usado quando `WP_REDIS_SCHEME=unix` (nesse caso `host`/`port` são descartados) |
| `WP_REDIS_SCHEME` | `tcp` | `tcp` / `unix` / `tls` (`rediss`) |
| `WP_REDIS_DATABASE` | `0` | Índice da base lógica Redis (0-15 por omissão) — usado no isolamento entre sites (secção 2) |
| `WP_REDIS_PASSWORD` | — | String (só password) ou array `[username, password]` para ACL Redis 6+. Mascarado como `••••••••` em `wp redis status`/diagnostics |
| `WP_REDIS_USERNAME` | — | Username ACL explícito; sobrepõe-se ao `[0]` de `WP_REDIS_PASSWORD` quando ambos definidos (só cliente Predis) |
| `WP_REDIS_TIMEOUT` | `1` | Timeout de ligação, em segundos |
| `WP_REDIS_READ_TIMEOUT` | `1` | Timeout de leitura, em segundos |
| `WP_REDIS_RETRY_INTERVAL` | `null` | Intervalo entre tentativas de reconexão, em ms |
| `WP_REDIS_SSL_CONTEXT` | — | Array de opções de stream context PHP para TLS, ex. `['verify_peer' => false]` |
| `WP_REDIS_CLIENT` | auto (`phpredis` se a extensão existir, senão `predis`) | `phpredis` / `pecl` (alias de `phpredis`) / `relay` / `credis` / `predis` — força o cliente |
| `WP_REDIS_IGBINARY` | `false` | bool — usa serialização `igbinary` se a extensão `igbinary` estiver carregada |
**Mecanismo:** `build_parameters()` em `object-cache.php` itera um array fixo
de settings (`scheme, host, port, path, password, database, timeout,
read_timeout, retry_interval`) e resolve cada um via
`sprintf('WP_REDIS_%s', strtoupper($setting))` — é assim, literalmente, que
o nome de cada constante é gerado a partir do parâmetro Redis correspondente.
### Alta disponibilidade / escala
| Constante | Descrição |
|---|---|
| `WP_REDIS_CLUSTER` | Array de DSNs (ou string = id de ligação nomeada) — activa modo Redis Cluster |
| `WP_REDIS_SHARDS` | Array de servidores — sharding client-side via `RedisArray` (só cliente `phpredis`) |
| `WP_REDIS_SENTINEL` | Nome do serviço Sentinel — requer `WP_REDIS_SERVERS` com os hosts dos sentinels |
| `WP_REDIS_SERVERS` | Array de servidores — replicação Predis (`options['replication']='predis'`) ou lista de sentinels quando `WP_REDIS_SENTINEL` também está definida |
Nenhuma destas está em uso no bundle Descomplicar® (todos os sites usam uma
única instância Redis local, sem cluster/replicação) — documentado aqui por
cobertura completa do plugin, não por uso confirmado em produção.
### Comportamento da cache
| Constante | Default | Descrição |
|---|---|---|
| `WP_REDIS_PREFIX` | — | Prefixo de chave (isolamento, ver secção 2); herda de `WP_CACHE_KEY_SALT` se esta estiver definida e `WP_REDIS_PREFIX` não estiver; em Cloudways é preenchido automaticamente a partir de `HTTP_X_APP_USER` |
| `WP_REDIS_SELECTIVE_FLUSH` | `false` | bool — `wp cache flush`/`flush()` só apaga chaves com o prefixo (script Lua via `SCAN`+`DEL`) em vez de `FLUSHDB` total à base lógica inteira |
| `WP_REDIS_GLOBAL_GROUPS` | 17 grupos por omissão (ver secção 9) | array — **substitui** (não soma) a lista de grupos partilhados entre sites de uma rede multisite |
| `WP_REDIS_IGNORED_GROUPS` | `[]` (vazio no plugin) | array — **substitui** a lista de grupos que nunca são persistidos no Redis |
| `WP_REDIS_UNFLUSHABLE_GROUPS` | `[]` (vazio) | array — **substitui** a lista de grupos que continuam em cache mas sobrevivem a um `flush()` |
| `WP_REDIS_MAXTTL` | — | int (segundos) — tecto aplicado a QUALQUER TTL, incluindo entradas "para sempre" (`expire=0`); `validate_expiration()` força o valor para o máximo sempre que `expiration === 0 \|\| expiration > max` |
| `WP_REDIS_FLUSH_TIMEOUT` | `5` | int (segundos) — timeout de socket aplicado só durante o flush selectivo/scripts Lua (`execute_lua_script()`) |
| `WP_REDIS_DISABLE_GROUP_FLUSH` | `false` | bool — `flush_group()` deixa de ser selectivo e cai num `flush()` total à base |
| `WP_REDIS_GRACEFUL` | `false` | bool — falha silenciosamente (sem excepção fatal) se o Redis estiver inacessível na ligação inicial |
| `WP_REDIS_DISABLED` | `false` | bool — desliga TODO o drop-in (nenhuma função `wp_cache_*` chega a ser definida, pois envolve o ficheiro inteiro num `if`); ver nuance de segurança na secção "Erros comuns" |
### Admin / UI / segurança
| Constante | Default | Descrição |
|---|---|---|
| `WP_REDIS_DISABLE_BANNERS` | `false` | bool — esconde todos os banners de upsell "Object Cache Pro" (dashboard, JS admin `disable_pro`, aviso WooCommerce) |
| `WP_REDIS_DISABLE_DROPIN_BANNERS` | `false` | bool — esconde só os avisos "drop-in em falta/desactualizado" no admin |
| `WP_REDIS_DISABLE_ADMINBAR` | `false` | bool — remove o nó "Object Cache" da barra de admin |
| `WP_REDIS_DISABLE_COMMENT` | `false` | bool — suprime o comentário HTML de rodapé "Performance optimized by Redis Object Cache" (hook `shutdown`) |
| `WP_REDIS_MANAGER_CAPABILITY` | `manage_options` (`manage_network_options` em multisite) | string — sobrepõe-se à capability necessária para gerir o plugin (menu, acções, admin bar); passa também pelo filtro `redis_cache_manager_capability` |
| `WP_REDIS_DISABLE_DROPIN_CHECK` | `false` | bool — salta o teste completo de escrita (copiar/verificar versão/apagar ficheiro `object-cache.tmp`), só confirma existência+permissão de escrita do drop-in já instalado |
| `WP_REDIS_DISABLE_DROPIN_AUTOUPDATE` | `false` | bool — desliga a auto-actualização do drop-in em `admin_init` quando a versão instalada do plugin muda |
### Métricas
| Constante | Default | Descrição |
|---|---|---|
| `WP_REDIS_DISABLE_METRICS` | `false` | bool — desliga completamente a recolha de métricas (esconde tab "Metrics", widget de dashboard, secção da admin bar) |
| `WP_REDIS_METRICS_MAX_TIME` | `3600` (`HOUR_IN_SECONDS`) | int (segundos) — janela de retenção/visualização das métricas; usado tanto para o gráfico como para a purga via cron `rediscache_discard_metrics` |
### Caminho do plugin
| Constante | Descrição |
|---|---|
| `WP_REDIS_PLUGIN_PATH` | Só tem efeito se definida **antes** do ficheiro principal do plugin carregar (`if (!defined('WP_REDIS_PLUGIN_PATH')) define(...)`) — é o mecanismo que o bundle partilhado Descomplicar® usa para apontar o drop-in de cada site para a cópia partilhada do plugin em vez de `wp-content/plugins/redis-cache` local |
---
## 7. Grupos de cache — semântica exacta dos 3 tipos
O construtor de `WP_Object_Cache` (`object-cache.php` linhas ~508-529) usa
três arrays distintos, cada um com comportamento próprio:
| Tipo | Efeito | Constante de override | Lista por omissão do plugin |
|---|---|---|---|
| `global_groups` | Partilhado entre todos os sites de uma rede multisite (não leva `blog_prefix`) | `WP_REDIS_GLOBAL_GROUPS` (substitui) | 17 grupos: `blog-details, blog-id-cache, blog-lookup, global-posts, networks, rss, sites, site-details, site-lookup, site-options, site-transient, users, useremail, userlogins, usermeta, user_meta, userslugs` (+ `redis-cache` sempre adicionado) |
| `ignored_groups` | Nunca chega a ir para o Redis — fica só em memória do pedido actual | `WP_REDIS_IGNORED_GROUPS` (substitui) | **`[]` vazio no próprio plugin** |
| `unflushable_groups` | Fica em cache normalmente, mas sobrevive a um `flush()`/`wp cache flush` | `WP_REDIS_UNFLUSHABLE_GROUPS` (substitui) | `[]` vazio |
**Correcção/precisão sobre a secção 5 acima:** os 5 "grupos ignorados por
omissão" vistos em `wp redis status`
(`counts, plugins, theme_json, WPForms_Entry_Handler, themes`) **não são
configuração do plugin `redis-cache`** — o array `$ignored_groups` do
próprio plugin nasce vazio. São chamadas em runtime à função wrapper
`wp_cache_add_non_persistent_groups()` (que faz `array_merge`, não
substitui) feitas por:
- **WordPress core**, em `wp-includes/load.php::wp_start_object_cache()`
(confirmado ao vivo nesta expansão): `wp_cache_add_non_persistent_groups(
['counts', 'plugins', 'theme_json'] )`, logo a seguir a carregar o drop-in;
- **WordPress core**, em `wp-includes/class-wp-theme.php:264`:
`wp_cache_add_non_persistent_groups( 'themes' )`;
- **Plugin WPForms** (`WPForms_Entry_Handler`), específico deste site — noutro
site sem WPForms este grupo não aparece.
Ou seja: a lista de grupos ignorados varia por site consoante os plugins
activos, não é fixa do `redis-cache`. Da mesma forma, o WordPress core
regista o seu próprio conjunto (mais amplo, 22 grupos incluindo
`blog_meta`, `image_editor`, `network-queries`, `site-queries`,
`theme_files`, `translation_files`, `user-queries`) via
`wp_cache_add_global_groups()` no mesmo `wp_start_object_cache()`, que se
soma (`array_unique(array_merge(...))`) aos 17 do plugin.
---
## 8. Arquitectura — zero `wp_options`, tudo por constante
Confirmado ao vivo nesta expansão:
```bash
wp option list --search='*redis*' --path=$PATH --format=json # []
wp option list --search='*rediscache*' --path=$PATH --format=json # []
```
O `redis-cache` **não grava nenhuma opção na base de dados**. Não há página
de definições com formulário — `includes/ui/settings.php` é só um wrapper
de tabs (Overview/Metrics/Diagnostics) que lê constantes e o estado ao vivo
do Redis. Toda a configuração vive em `wp-config.php` (constantes) e o único
estado persistente do plugin é:
- o próprio ficheiro drop-in `wp-content/object-cache.php` (presença =
cache activa);
- as métricas, gravadas **dentro do próprio Redis** como sorted set na chave
`<prefix>metrics:redis-cache` (via `ZADD`/`ZRANGEBYSCORE`), purgadas por
hora via cron `rediscache_discard_metrics` — não em `wp_options` nem em
ficheiro.
Isto tem implicação directa para migração/clonagem de sites: copiar
`wp_options` entre sites nunca traz nem perde configuração deste plugin —
só o `wp-config.php` (constantes) e o `wp-content/object-cache.php`
(presença do drop-in) importam.
---
## 9. Interface admin — o que existe na prática
- **3 tabs** na página `Definições → Redis` (`options-general.php?page=redis-cache`,
ou `settings.php?page=redis-cache` em multisite): **Overview**, **Metrics**
(desactivado se `Metrics::is_enabled()` for falso, i.e.
`WP_REDIS_DISABLE_METRICS`), **Diagnostics** (o mesmo output de
`wp redis status`, renderizado em HTML).
- **Admin bar**: nó "Object Cache" (removível com `WP_REDIS_DISABLE_ADMINBAR`)
com submenu "Flush Cache" (AJAX, acção `roc_flush_cache`) e "Settings";
mostra hit-ratio/hits/misses/tamanho da página actual em hover.
- **Widget de dashboard** ("Redis Object Cache"), só visível a quem tem a
capability de gestão e só se `Metrics::is_enabled()`.
- **Integração Query Monitor**: regista um collector/output próprio
(`class-qm-collector.php`, `class-qm-output.php`) que acrescenta um painel
"Cache" ao Query Monitor quando este plugin está activo — automático, sem
configuração.
- **Aviso específico WooCommerce**: notice de upsell Pro nas páginas de
encomendas/produtos/analytics do WooCommerce (silenciável com
`WP_REDIS_DISABLE_BANNERS`).
### Acções da página de definições vs comandos WP-CLI
A página admin usa links com nonce (`?action=<x>&_wpnonce=...`) que chamam
exactamente a mesma lógica dos comandos WP-CLI:
| Acção admin (`action=`) | Equivalente WP-CLI | Efeito |
|---|---|---|
| `enable-cache` | `wp redis enable` | Copia `includes/object-cache.php` para `wp-content/object-cache.php` |
| `disable-cache` | `wp redis disable` | Apaga `wp-content/object-cache.php` |
| `update-dropin` | `wp redis update-dropin` | Reescreve o drop-in com a versão actual do plugin |
| `flush-cache` | `wp cache flush` | Chama `wp_cache_flush()` directamente (sem passar pelo WP-CLI genérico) |
---
## 10. Hooks disponíveis para integração
| Hook | Tipo | Quando dispara |
|---|---|---|
| `redis_object_cache_enable` | action | Depois de copiar o drop-in ao activar (via admin ou `wp redis enable`) — argumento: `bool $result` |
| `redis_object_cache_disable` | action | Depois de apagar o drop-in ao desactivar |
| `redis_object_cache_update_dropin` | action | Depois de `wp redis update-dropin` / auto-update |
| `redis_object_cache_flush` | action | Em cada `flush()` — argumentos: `$results, $deprecated, $selective, $salt, $execute_time` |
| `redis_cache_manager_capability` | filter | Sobrepõe a capability de gestão (default `manage_options`/`manage_network_options`), além/em vez de `WP_REDIS_MANAGER_CAPABILITY` |
| `redis_cache_validate_dropin` | filter | Sobrepõe a validação de "o drop-in instalado é o deste plugin" (compara `PluginURI`) |
| `redis_cache_add_non_persistent_groups` | filter | Filtra a lista de grupos antes de `add_non_persistent_groups()` os fundir em `ignored_groups` |
---
## 11. Auto-resolução de conflitos com outros plugins de cache
O plugin desactiva proactivamente os object caches nativos de outros
plugins de cache/performance para evitar dois drop-ins a lutar pelo mesmo
ficheiro:
```php
add_filter( 'perflab_disable_object_cache_dropin', '__return_true' ); // WordPress Performance Lab
add_filter( 'w3tc_config_item_objectcache.enabled', '__return_false' ); // W3 Total Cache
add_action( 'litespeed_init', [ $this, 'litespeed_disable_objectcache' ] ); // LiteSpeed Cache
```
Relevante para o bundle Descomplicar®: confirma que activar `redis-cache`
num site que também tenha W3TC ou LiteSpeed Cache instalado (mesmo que
inactivo o respectivo módulo de object cache) não gera conflito — o
`redis-cache` desliga automaticamente a componente de object cache desses
plugins via filtro, sem intervenção manual.
## Erros comuns
| Sintoma | Causa | Solução |
|---|---|---|
| `wp redis flush` → "not a registered command" | Subcomando não existe no plugin | Usar `wp cache flush` (genérico WP-CLI, funciona via drop-in activo) |
| Dados alterados por `wp db query` não aparecem no site | Redis serve versão em cache, SQL directo não invalida | `wp cache flush --path=$PATH` logo a seguir a qualquer escrita SQL |
| `wp redis status` mostra `Client: Predis`, não `PhpRedis` | Extensão PHP nativa `redis` não instalada no servidor | Não é erro — Predis é fallback 100% funcional, apenas mais lento; upgrade futuro se a extensão for instalada |
| Dois sites com o mesmo `WP_REDIS_DATABASE` (colisão) | `wp-config.php` copiado entre sites sem ajustar `WP_REDIS_DATABASE`/`WP_REDIS_PREFIX` | Correr a verificação da secção 2 após qualquer clone/migração de site no mesmo servidor; atribuir DB livre (0-15) e prefixo único |
| `wp redis status` → `Status: Não ligado` mas plugin activo | Drop-in `object-cache.php` ausente/removido de `wp-content/` (ex.: apagado por engano numa migração) | `wp redis enable` reescreve o drop-in |
| Confundir "plugin activo" com "cache activa" | `wp plugin is-active redis-cache` só confirma o plugin, não o drop-in | Confirmar sempre com `wp redis status` → `Ping: PONG` |
| Definir `WP_REDIS_DISABLED=true` sem remover o drop-in | Comportamento intencional, não um bug | Não é preciso apagar `object-cache.php`; a constante sozinha faz com que nenhuma função `wp_cache_*` seja definida pelo drop-in, e o WordPress core (`wp-includes/load.php::wp_start_object_cache()`) deteta isso e carrega automaticamente o fallback nativo `wp-includes/cache.php` — confirmado por leitura do core nesta expansão |
| `wp redis status` → grupos ignorados diferentes de site para site | Não é config do plugin — WordPress core adiciona `counts/plugins/theme_json/themes` e plugins activos (ex. WPForms → `WPForms_Entry_Handler`) via `wp_cache_add_non_persistent_groups()` em runtime | Normal; comparar só via `WP_REDIS_IGNORED_GROUPS` se quiseres saber o que o `redis-cache` em si define (por omissão, nenhum) |
@@ -0,0 +1,526 @@
---
name: seguranca-descomplicar
description: Gestão do plugin próprio "Segurança Descomplicar" (seguranca-descomplicar, v1.4.2) via WP-CLI e REST API nos 7 sites do bundle CWP. Cobre os 5 módulos — Security Headers (CSP/XFO/security.txt), Wordfence Agent (endpoint REST autenticado para sites sem SSH), Login/dash (URL de login customizada), Descomplicar Monitor (telemetria para dash.descomplicar.pt), Admin Hardening (Classic Editor, emails de update, nags) — com comandos `wp seguranca-descomplicar`, options `segdesc_*`, e gotchas reais de produção. Usar quando "seguranca-descomplicar", "plugin de segurança Descomplicar", "CSP", "X-Frame-Options", "security.txt", "wordfence agent", "login dash", "/dash", "descomplicar monitor", "admin hardening", "classic editor forçado", "esconder avisos de update", "token do agente", "allowlist IP wordfence agent".
---
# /seguranca-descomplicar — Plugin próprio de segurança WordPress (5 módulos)
Plugin normal (não mu-plugin, decisão explícita), instalado em
`wp-content/plugins/seguranca-descomplicar/`, **gerido inteiramente pela
Descomplicar®** — código-fonte único, versionado em
`71.Seguranca/plugin/seguranca-descomplicar/seguranca-descomplicar.php`,
distribuído por SCP/cp aos 7 sites do bundle. Substitui: `wordsense-agent.php`
(plugin antigo), mu-plugin `descomplicar-security-headers.php`, plugin
autónomo `descomplicar-monitor`, e todos os snippets WPCode/Code Snippets
de login `/dash` e admin hardening que estavam clonados byte-a-byte em 6 dos
7 sites.
**Fonte:** código-fonte completo lido nesta sessão (1494 linhas) +
`CONFIG-Plugins-Referencia.md` §2 (config ao vivo verificada nos 7 sites) +
`index.md` (changelog de 8 sessões de produção, bugs reais e correcções).
**Gerido/consumido por (ferramentas externas, fora desta skill):**
`71.03-SecHeaders/tools/secheaders.py` (módulo 1) e
`71.01-Wordsense/tools/wordsense.py`, transporte `http` (módulo 2).
---
## Contexto CWP — sempre obrigatório
```bash
# Local (quando o path existe no filesystem do agente)
wp seguranca-descomplicar <comando> --path=$PATH
# Via SSH (servidor real)
ssh -p 9443 root@server.descomplicar.pt \
"wp seguranca-descomplicar <comando> --path=$PATH --allow-root"
```
Namespace REST do plugin: `seguranca-descomplicar/v1` (todos os endpoints
exigem `Authorization: Bearer <token>`, token de 64 chars hex em
`segdesc_agent_token`).
---
## Decision tree — qual módulo/comando usar
| Preciso de | Módulo | Comando/opção |
|---|---|---|
| Ver/mudar CSP, XFO, security.txt | 1 | `wp seguranca-descomplicar headers status\|set` |
| Diagnosticar/ajustar Wordfence sem SSH (via HTTP) | 2 | REST `seguranca-descomplicar/v1/wordfence/*` |
| Ligar/mudar a URL de login custom | 3 | `wp seguranca-descomplicar login status\|enable\|disable\|slug\|block-wp-login` |
| Ver estado da telemetria para o dashboard | 4 | `wp option get descomplicar_monitor_*` (sem comando CLI próprio) |
| Ligar/desligar Classic Editor, emails de update, nags | 5 | `wp seguranca-descomplicar hardening status\|enable\|disable` |
| Rodar o token do agente REST | — | `wp seguranca-descomplicar token --rotate` |
| Restringir o REST endpoint a IPs específicos | — | `wp seguranca-descomplicar allowlist --set=IP1,IP2` |
---
## Referência de constantes (`SEGDESC_*`)
Todas as constantes definidas no plugin (topo do ficheiro, antes de
qualquer hook), agrupadas por função — confirma cobertura total do
código-fonte (26 constantes no total).
**Identificação**
| Constante | Valor | Nota |
|---|---|---|
| `SEGDESC_VERSION` | `'1.4.2'` | Devolvida em `wordfence/status.agent_version` |
| `SEGDESC_NS` | `'seguranca-descomplicar/v1'` | Namespace REST |
**Options (`segdesc_*`, persistidas em `wp_options`)** — ver tabelas de
cada módulo abaixo para defaults/comportamento; nomes reais:
`segdesc_agent_token`, `segdesc_headers_enabled`, `segdesc_csp`,
`segdesc_xfo`, `segdesc_csp_report_only`, `segdesc_security_txt`,
`segdesc_ip_allowlist`, `segdesc_nginx_csp_covered`,
`segdesc_login_enabled`, `segdesc_login_slug`,
`segdesc_login_block_wplogin`, `segdesc_hardening_enabled`,
`segdesc_hardening_classic_editor`, `segdesc_hardening_update_emails`,
`segdesc_hardening_update_nags`.
**Defaults/valores hardcoded (literais PHP usados como fallback, não são options)**
| Constante | Valor |
|---|---|
| `SEGDESC_DEFAULT_LOGIN_SLUG` | `'dash'` |
| `SEGDESC_DEFAULT_CSP` | CSP de enforcement (ver secção 1) |
| `SEGDESC_DEFAULT_XFO` | `'SAMEORIGIN'` |
| `SEGDESC_DEFAULT_CSP_REPORT_ONLY` | CSP Report-Only (ver secção 1) |
| `SEGDESC_DANGEROUS_KEYS` | `serialize(['waf_status', 'isPaid', 'apiKey'])` — chaves `wfconfig` que exigem `force_dangerous=true` em `/wordfence/config` |
**Rate-limit do endpoint REST** (só editável no código, sem option/wp-cli)
| Constante | Valor | Nota |
|---|---|---|
| `SEGDESC_RATE_LIMIT_WINDOW` | `60` (segundos) | Janela do tecto geral de pedidos |
| `SEGDESC_RATE_LIMIT_MAX` | `60` | Pedidos nominais/IP/janela (~30 reais, ver nota da dupla invocação) |
| `SEGDESC_RATE_LIMIT_FAIL_MAX` | `10` | Falhas nominais/IP/janela antes do lockout (~5 reais) |
| `SEGDESC_RATE_LIMIT_FAIL_LOCKOUT` | `900` (segundos = 15 min) | Duração do lockout por IP |
---
## 1. Security Headers — CSP, X-Frame-Options, security.txt
Hook `send_headers` (envia CSP/XFO/CSP-Report-Only) + hook `init` (serve
`/.well-known/security.txt`, RFC 9116).
### Options (`segdesc_*`)
| Option | Default | Nota |
|---|---|---|
| `segdesc_headers_enabled` | `'1'` (definida na activação) | Liga/desliga o módulo inteiro |
| `segdesc_csp` | `SEGDESC_DEFAULT_CSP` (definida na activação) | CSP de **enforcement** — só é enviada se `nginx_csp_covered` ≠ `'1'` |
| `segdesc_xfo` | `SAMEORIGIN` (definida na activação) | Sempre enviada, independente do nginx |
| `segdesc_csp_report_only` | `SEGDESC_DEFAULT_CSP_REPORT_ONLY` (fallback em código, não persistida na activação) | Sempre enviada — telemetria; único mecanismo de report em sites sem template nginx |
| `segdesc_security_txt` | `''` | Sem default — vazio = ficheiro não é servido; conteúdo bruto do `.well-known/security.txt` |
| `segdesc_ip_allowlist` | `''` (vazio = sem restrição) | CSV de IPs — só afecta os endpoints REST do módulo 2, não o site público |
| `segdesc_nginx_csp_covered` | `'0'` | **`'1'` em sites com o template nginx aplicado** — desliga o CSP de enforcement do plugin (evita intersecção de directivas entre 2 CSPs); XFO continua sempre activo |
### Comandos
```bash
# Estado
wp seguranca-descomplicar headers status --path=$PATH
# Actualizar CSP/XFO
wp seguranca-descomplicar headers set --csp="default-src 'self'; script-src 'self' https:" --path=$PATH
wp seguranca-descomplicar headers set --xfo=SAMEORIGIN --path=$PATH
# security.txt a partir de ficheiro local
wp seguranca-descomplicar headers set --security-txt-file=/tmp/security.txt --path=$PATH
# Ligar/desligar o módulo inteiro
wp seguranca-descomplicar headers set --enabled=1 --path=$PATH
# Opções que o wp-cli custom NÃO cobre — usar wp option directo
wp option get segdesc_nginx_csp_covered --path=$PATH
wp option update segdesc_nginx_csp_covered 1 --path=$PATH # sites com template nginx aplicado
wp option get segdesc_ip_allowlist --path=$PATH
```
### CSP real em produção (v1.4.2 — permissivo por directiva, sem lista branca de domínios)
```
default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval' https:;
style-src 'self' 'unsafe-inline' https:;
img-src 'self' data: https:;
font-src 'self' data: https:;
frame-src https:;
frame-ancestors 'self';
object-src 'none'
```
Decisão explícita do utilizador (10ª sessão): uma lista branca por domínio
(`frame-src youtube.com google.com ...`) é manutenção infinita — cada embed
novo obriga a voltar a editar. `frame-src https:` permite qualquer iframe
servido por HTTPS; o que interessa bloquear é `http://` não encriptado e
`data:`/`javascript:`. `object-src 'none'` e `frame-ancestors 'self'`
mantêm-se restritos (sem caso de uso legítimo / protecção anti-clickjacking
deste site).
**Nota de produção (16-08-2026):** na prática, no bundle CWP, CSP/XFO são
geridos maioritariamente pelo **template nginx** (`71.03-SecHeaders/`,
nível de servidor, sobrevive a cache/preload de plugin) nos sites que o
têm aplicado — o módulo do plugin fica então limitado a `security.txt` +
CSP Report-Only (telemetria, ver `segdesc_nginx_csp_covered`). Em
`emanuelalmeida.pt` (piloto desde a 11ª sessão), os headers passaram a
ser servidos por **Cloudflare Transform Rules** (token do plugin App for
Cloudflare®) — nginx e este plugin continuam a enviar os seus próprios
headers, mas ficam invisíveis ao tráfego real porque o `set` do
Cloudflare se sobrepõe antes de chegar ao browser; servem só de fallback
para acesso directo à origem (bypass do Cloudflare).
---
## 2. Wordfence Agent — endpoint REST autenticado (sites sem SSH)
8 rotas REST sob `seguranca-descomplicar/v1`, autenticação partilhada via
`segdesc_check_auth()` (allowlist → lockout → rate-limit → Bearer token).
Registadas sempre; `/wordfence/status` indica se o Wordfence está mesmo
activo no site.
| Rota | Método | Payload | Função |
|---|---|---|---|
| `/headers/status` | GET | — | Estado do módulo 1 |
| `/headers/set` | POST | `csp`, `xfo`, `security_txt`, `enabled` | Ajusta módulo 1 remotamente |
| `/wordfence/status` | GET | — | `wordfence_active`, `version`, `db_prefix` |
| `/wordfence/query` | POST | `sql` | **Só SELECT/SHOW contra exactamente 1 tabela `wf*`** — bloqueia JOIN, subquery, UNION, INSERT/UPDATE/DELETE/DROP/etc. |
| `/wordfence/config` | POST | `key`, `value`, `force_dangerous` | UPSERT em `{prefix}wfconfig`; chaves perigosas (`waf_status`, `isPaid`, `apiKey`) exigem `force_dangerous=true` |
| `/wordfence/unblock-ip` | POST | `ip` | `DELETE FROM {prefix}wfblockediplog WHERE IP = INET6_ATON(ip)` |
| `/wordfence/whitelist-ip` | POST | `ip`, `reason` | Acrescenta `"{ip} #{reason}"` a `neverBlockIP` em `wfconfig` |
| `/wordfence/scan` | POST | — | `wfScanEngine::startScan()` se a classe existir; senão `do_action('wordfence_doScan')` (fallback cron) |
**`/headers/*` — nota de proveniência:** os 2 endpoints REST de headers
(`status`/`set`) foram desenhados para o transporte `http` de
`71.03-SecHeaders/tools/secheaders.py` (comentário no código-fonte:
"usados por secheaders.py transporte 'http' (futuro, hoje só SSH)") —
presentes e funcionais, mas **ainda não consumidos em produção**:
`secheaders.py` aplica correcções via SSH/wp-cli directo, não via este
REST. Único consumidor real hoje é o próprio `wp seguranca-descomplicar
headers`.
### Autenticação e rate-limit (importante para clientes REST)
```
Authorization: Bearer <segdesc_agent_token>
```
- **Allowlist de IP** (opcional, `segdesc_ip_allowlist`) verificada primeiro.
- **Rate-limit** por IP: 60 pedidos/60s nominal — **mas o `WP_REST_Server`
invoca `permission_callback` DUAS VEZES por pedido HTTP real** (confirmado
ao vivo em `carstuff.pt`), pelo que o limite prático é ~30 pedidos reais,
não 60.
- **Lockout por falhas de auth:** 10 falhas nominais (~5 reais) → lockout de
15 min (900s), chaveado por IP via transient.
- **IP do cliente** (`segdesc_client_ip()`) só confia em `X-Forwarded-For`
quando `REMOTE_ADDR` é um proxy local confiável (`127.0.0.1`/`::1`) — as
portas backend do CWP (2083/2031/2095) são acessíveis directamente da
internet, por isso um pedido directo ao backend usa sempre `REMOTE_ADDR`,
nunca o XFF do atacante.
### Comandos WP-CLI relacionados
```bash
# Ver/rodar o token de autenticação do agente
wp seguranca-descomplicar token --path=$PATH
wp seguranca-descomplicar token --rotate --path=$PATH
# Allowlist de IP para os endpoints REST
wp seguranca-descomplicar allowlist --path=$PATH
wp seguranca-descomplicar allowlist --set=203.0.113.5,203.0.113.6 --path=$PATH
wp seguranca-descomplicar allowlist --clear --path=$PATH
```
### Exemplo de pedido real (curl)
```bash
TOKEN=$(wp seguranca-descomplicar token --path=$PATH)
curl -s https://site.pt/wp-json/seguranca-descomplicar/v1/wordfence/status \
-H "Authorization: Bearer $TOKEN"
```
---
## 3. Login /dash — URL de login customizada e segura
Substitui os antigos snippets WPCode "Alterar /wp-admin para /dash" (que
deixaram de executar por `E_DEPRECATED` em PHP 8.2 — ver Gotchas). Handler
em `template_redirect` prioridade 1 (antes do `redirect_canonical`,
prioridade 10).
### Comportamento quando `segdesc_login_enabled = '1'`
- `/<slug>` (default `dash`): mostra o form nativo de login WP se não
autenticado; `wp_redirect(admin_url())` se autenticado.
- `wp-login.php` directo (GET): **404** para não autenticados. O **POST**
do form (submissão real do login) continua a funcionar — o bloqueio só
intercepta GET.
- `/wp-admin/` directo: **404** para não autenticados, excepto
`admin-ajax.php` (necessário para plugins front-end).
### Hooks internos
| Hook | Prioridade | Função |
|---|---|---|
| `init` → `segdesc_login_rewrite_rule` | default (10) | Regista `add_rewrite_rule('^<slug>/?$', 'index.php?segdesc_login=1', 'top')` — só se o módulo estiver ligado |
| `query_vars` → `segdesc_login_query_var` | default (10) | Regista a query var `segdesc_login` (necessária para o WP reconhecer o parâmetro da rewrite rule) |
| `template_redirect` → `segdesc_login_handle` | **1** | Handler principal — antes do `redirect_canonical` (prioridade 10) |
| `init` → `segdesc_login_block_wp_login` | default (10) | Bloqueio de `wp-login.php` directo (GET) |
| `init` → `segdesc_login_block_wp_admin` | default (10) | Bloqueio de `/wp-admin/` directo (excepto `admin-ajax.php`) |
| `update_option_segdesc_login_slug` → `segdesc_login_flush_rules_on_slug_change` | default (10) | `flush_rewrite_rules()` automático sempre que `segdesc_login_slug` muda — **mesmo por `wp option update` directo**, não só pelo subcomando `login slug` |
### Options
| Option | Default | Nota |
|---|---|---|
| `segdesc_login_enabled` | `'0'` | Módulo desligado por omissão — só activo onde pedido |
| `segdesc_login_slug` | `'dash'` (constante `SEGDESC_DEFAULT_LOGIN_SLUG`, não persistida a menos que alterada) | Sanitizado para `[a-z0-9_-]` |
| `segdesc_login_block_wplogin` | `'1'` | Bloqueia `wp-login.php` directo (GET) |
### Comandos
```bash
wp seguranca-descomplicar login status --path=$PATH
wp seguranca-descomplicar login enable --path=$PATH # faz flush_rewrite_rules() automático
wp seguranca-descomplicar login disable --path=$PATH
wp seguranca-descomplicar login slug --set=acesso --path=$PATH
wp seguranca-descomplicar login block-wp-login --on --path=$PATH
wp seguranca-descomplicar login block-wp-login --off --path=$PATH
```
`status` devolve `enabled`, `slug`, `block_wp_login`, `login_url` (URL
completo já resolvido com `home_url()`).
**Nota carstuff.pt (achado 10ª sessão, fora do âmbito deste módulo):**
`/dash` naquele site serve login e `wp-login.php` redirecciona para
`/dash` por um mecanismo que **não é o Módulo 3** (nunca activado em
carstuff.pt até à migração completa) nem o snippet WPCode legado (sempre
em draft) — candidato mais provável é o `branda-white-labeling`
(login-screen custom). Login do admin funciona normalmente; não confundir
com este módulo ao diagnosticar `/dash` nesse site especificamente.
---
## 4. Descomplicar Monitor — telemetria para dash.descomplicar.pt
Classe `Descomplicar_Monitor` (singleton), **funde o antigo plugin autónomo
`descomplicar-monitor`** — reutiliza as MESMAS options
(`descomplicar_monitor_*`, prefixo diferente do resto do plugin, propositado
para preservar dados na migração) e o MESMO cron
(`descomplicar_monitor_cron`). Guard
`is_plugin_active('descomplicar-monitor/descomplicar-monitor.php')`: só
instancia se o plugin antigo não estiver activo (evita duplicar envios).
**Sem comando WP-CLI próprio** — este módulo não regista `WP_CLI::add_command`.
Interage-se via `wp option`, `wp eval`, ou a página de definições em
**Definições → Descomplicar Monitor** (`add_options_page`, capability
`manage_options`; grupo de settings `descomplicar_monitor_settings` via
`register_settings`, hook `admin_init`; AJAX `descomplicar_monitor_test` /
`descomplicar_monitor_send_now`, ambos com nonce próprio via
`check_ajax_referer`).
### Hooks internos
| Hook | Função |
|---|---|
| `admin_menu` → `add_admin_menu` | Regista a página **Definições → Descomplicar Monitor** |
| `admin_init` → `register_settings` | Regista o grupo `descomplicar_monitor_settings` (campos `api_key`, `enabled`) |
| `descomplicar_monitor_cron` → `send_monitoring_data` | Handler do cron — envia a telemetria |
| `cron_schedules` → `add_cron_interval` | Regista um intervalo **próprio** chamado `twice_daily` (43200s = 12h) — **não é o `twicedaily` nativo do WordPress** (nomes diferentes, mesmo valor); `wp_schedule_event` usa este nome custom |
| `wp_ajax_descomplicar_monitor_test` → `ajax_test_connection` | AJAX "Testar Conexão" |
| `wp_ajax_descomplicar_monitor_send_now` → `ajax_send_now` | AJAX "Enviar Agora" |
| `init` → `ensure_cron` | Reagenda `descomplicar_monitor_cron` se `wp_next_scheduled()` devolver vazio |
**Sem `register_deactivation_hook`** (nenhum módulo do plugin tem) — ao
desactivar o plugin, o evento `descomplicar_monitor_cron` continua
agendado no wp-cron, mas o código deixa de carregar, logo o disparo do
evento não faz nada (sem handler registado) — consome um ciclo de cron
sem efeito, sem erro visível. Reactivar retoma o envio automaticamente,
sem reconfigurar nada (options preservadas).
### Options
| Option | Nota |
|---|---|
| `descomplicar_monitor_enabled` | `'1'`/`'0'` — sem module flag `segdesc_*` (nome legado preservado) |
| `descomplicar_monitor_api_key` | Chave enviada como header `X-API-Key` |
| `descomplicar_monitor_last_sent` | Timestamp `Y-m-d H:i:s`, actualizado em cada tentativa (sucesso ou falha) |
| `descomplicar_monitor_last_status` | `'success'` ou a mensagem de erro |
### Dados recolhidos (POST JSON para `https://dash.descomplicar.pt/api/wp-monitor`)
URL, nome, **email de admin**, versão WP/PHP/MySQL, **flag multisite**,
**timestamp ISO 8601** (`current_time('c')`) e **timezone** do site, tema
(+ parent), lista de plugins activos c/ versão, updates pendentes
(core/plugins/themes, com bloco `counts` agregado), contagem de
posts/páginas/utilizadores/comentários, estatísticas da BD (**nº de
tabelas `{prefix}*`**, tamanho em MB, **prefix**, transients expirados,
tamanho do autoload em KB), memory_limit, upload máximo, debug mode, SSL,
cron disabled, cache habilitada, e um bloco `health` (status
good/warning/critical com lista de issues: `WP_DEBUG` sem log, sem SSL,
uploads sem permissão de escrita, memória PHP < 64MB).
**Nota:** `get_pending_updates()` chama `wp_update_plugins()` e
`wp_update_themes()` directamente antes de ler `get_plugin_updates()`/
`get_theme_updates()` — força uma verificação síncrona a
`api.wordpress.org` em **cada execução do cron de 12h**, mesmo que o
WordPress já tivesse um resultado em cache válido. Corre em contexto de
cron (não bloqueia carregamento de página), por isso não é o mesmo
problema de performance do Gotcha #3 (que era em `admin_init`/page load).
### Comandos úteis
```bash
# Estado
wp option get descomplicar_monitor_enabled --path=$PATH
wp option get descomplicar_monitor_last_status --path=$PATH
wp option get descomplicar_monitor_last_sent --path=$PATH
# Forçar envio imediato (fora do cron 12h)
wp eval 'echo json_encode(Descomplicar_Monitor::get_instance()->send_monitoring_data());' --path=$PATH
# Confirmar que o cron está agendado (reagenda-se sozinho em cada 'init' se limpo)
wp cron event list --path=$PATH --fields=hook,next_run_relative | grep descomplicar_monitor_cron
```
---
## 5. Admin Hardening — Classic Editor, emails de update, nags
Substitui 3 snippets WPCode/Code Snippets clonados byte-a-byte (`md5sum`
idêntico) em 6 dos 7 sites CWP (origem: tutorial WPBeginner). O snippet de
emails tinha um bug real herdado: `add_filter('auto_core_update_send_email',
'wpb_stop_auto_update_emails', ...)` referenciava uma função nunca definida
(`wpb_stop_update_emails`, sem `_auto`), gerando `E_WARNING` silencioso em
cada auto-update. O módulo simplifica para `__return_false` directo.
### Options
| Option | Default (quando `hardening_enabled='1'`) | Nota |
|---|---|---|
| `segdesc_hardening_enabled` | `'0'` | Módulo desligado por omissão |
| `segdesc_hardening_classic_editor` | `'1'` | `use_block_editor_for_post{,_type}` → `__return_false` |
| `segdesc_hardening_update_emails` | `'1'` | `auto_{core,plugin,theme}_update_send_email` → `__return_false` |
| `segdesc_hardening_update_nags` | `'1'` | Wipe de `admin_notices`/`all_admin_notices`/`network_admin_notices` (incl. avisos Wordfence) + CSS para esconder badges de contagem |
### Comandos
```bash
wp seguranca-descomplicar hardening status --path=$PATH
wp seguranca-descomplicar hardening enable --path=$PATH
wp seguranca-descomplicar hardening enable --no-classic-editor --path=$PATH # liga o módulo mas deixa o bloco editor
wp seguranca-descomplicar hardening enable --no-update-emails --path=$PATH
wp seguranca-descomplicar hardening enable --no-update-nags --path=$PATH
wp seguranca-descomplicar hardening disable --path=$PATH
```
### Como o bloqueio de nags funciona de facto (decisão de negócio: agressivo)
Decisão do Emanuel: bloquear TAMBÉM avisos Wordfence e visibilidade de
updates pendentes — clientes reencaminhavam esses avisos/emails achando que
eram falha da Descomplicar. Dois mecanismos distintos, **nunca tocando nos
transients de update** (ver Gotchas #3):
1. **`in_admin_header` prioridade 999** → `unset($wp_filter['admin_notices'])`
(+ `all_admin_notices` + `network_admin_notices`). Tem de ser
`in_admin_header`, não `admin_init` (ver Gotchas #2).
2. **`admin_head`** → injecta `<style>` que esconde
`#adminmenu .update-plugins`, `#wpadminbar .update-plugins`,
`#adminmenu .wf-menu-badge` (o badge "Wordfence N" no menu não é um hook
— é HTML injectado directamente no título do item de menu por
`wordfenceClass.php`; só CSS apanha).
---
## Deploy aos 7 sites do bundle
Fonte do plugin canónico:
`71.Seguranca/plugin/seguranca-descomplicar/` → copiado para
`wp-content/plugins/seguranca-descomplicar/` em cada site
(`tools/distribuir_e_activar_plugin.py`: `cp -r` local se o `path` existir
no filesystem, senão `mkdir -p` + `scp -P 9443` + `ssh ... wp plugin
activate` via `root@server.descomplicar.pt`).
| Domínio | Conta CWP | Path |
|---|---|---|
| `descomplicar.pt` | `ealmeida` | `/home/ealmeida/public_html` |
| `emanuelalmeida.pt` | `ealmeida` | `/home/ealmeida/emanuelalmeida.pt` |
| `carstuff.pt` | `carstuff` | `/home/carstuff/public_html` |
| `familyclinic.pt` | `familycl` | `/home/familycl/public_html` |
| `ignitionvortex.pt` | `ignition` | `/home/ignition/public_html` |
| `solarfvengenharia.com` | `solarfv` | `/home/solarfv/public_html` |
| `watercontrol.pt` | `wtc` | `/home/wtc/public_html` |
```bash
python3 tools/distribuir_e_activar_plugin.py
```
`tools/executar_pipeline_completa.py` orquestra o ciclo completo de
segurança nos mesmos 7 domínios (`SITES_CONFIG`, campo `tipo:
"nossos_sites"`): 1) diagnóstico externo (`seguranca-check.py`) → 2)
diagnóstico de postura interno (`postura.py` → wordsense + softcheck) → 3)
correcção automática de headers (`secheaders.py`, template nginx ou este
plugin) → 4) re-análise de verificação → 5) relatório visual. Usa
`--confirm-fix` para aplicar correcções (sem a flag, é dry-run/diagnóstico).
**Estado ao vivo confirmado (16-08-2026):** 5 módulos activos e coerentes
nos 7 sites — `headers_enabled=1`, `login_enabled=1` (7/7, antes eram 5/7),
`hardening_enabled=1` (7/7), `bridge`/monitor migrado 7/7. Único desvio real
por site: `segdesc_nginx_csp_covered` só é `'1'` nos sites com o template
nginx aplicado (todos excepto casos pontuais em rollout).
**Scripts sem flags de plugin:** `distribuir_e_activar_plugin.py` não
aceita argumentos CLI — itera sempre a lista `SITES` completa hardcoded
(`cp -r` local se o `path` existir, senão `mkdir -p` + `scp -P 9443` +
`ssh ... wp plugin activate` via `root@server.descomplicar.pt`, porta
9443). `executar_pipeline_completa.py` só tem uma flag, `--confirm-fix`
(sem ela = dry-run/diagnóstico só); a lista `SITES_CONFIG` mapeia cada
domínio para `wordsense_site`/`softcheck_account` — usados por
`postura.py`/`wordsense.py` (fora desta skill), não por este plugin
directamente.
---
## Gotchas / erros reais corrigidos em produção
| # | Sintoma | Causa raiz | Correcção |
|---|---|---|---|
| 1 | Actualizar `post_content` de um snippet WPCode via `$wpdb->update` directo não desliga o snippet legado | O WPCode mantém uma cache compilada em `wp_options.wpcode_snippets`, separada de `wp_posts` — não invalida por escrita SQL directa | Apagar a option `wpcode_snippets` ou chamar `wpcode()->cache->cache_all_loaded_snippets()` depois de qualquer edição directa em BD |
| 2 | Aviso "ElementsKit Lite Premium" (ou outro plugin) sobrevive ao wipe de `admin_notices` | Alguns plugins registam o callback em `admin_notices` **de dentro** de outro callback pendurado em `admin_head` — que corre **depois** de `admin_init` na sequência de bootstrap (`admin_init → load-{page} → admin_head → in_admin_header → admin_notices`) | Wipe tem de estar em `in_admin_header` prioridade 999 (o último ponto seguro antes de `admin_notices` disparar), nunca em `admin_init` |
| 3 | Regressão de performance real: ~1,3s extra em **todo** o carregamento do wp-admin (medido: core 0,4s + plugins 0,5s + temas 0,3s) | Bloquear `pre_site_transient_update_*` para esconder nags impede a cache nativa de 12h do WordPress de "pegar" — `_maybe_update_core/plugins/themes()` vêem sempre "nunca verificado" e disparam pedido HTTP síncrono a `api.wordpress.org` em cada page load | Nunca tocar nos transients de update; esconder badges/contadores só por CSS (`.update-plugins`) |
| 4 | Badge "Wordfence N" no menu lateral continua visível apesar do wipe de `admin_notices` | Não vem de nenhum hook — é HTML injectado directamente no título do item de menu por `wordfenceClass.php` | Só CSS resolve: `#adminmenu .wf-menu-badge { display: none !important; }` |
| 5 | `/dash` dá 404, `wp-login.php` também dá 404 — login completamente inacessível | Snippet WPCode legado usava `basename(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH))` sem protecção — em PHP 8.2 `parse_url` pode devolver `null`, `basename(null)` dispara `E_DEPRECATED`; o WPCode apanha o erro e marca o snippet em `wpcode_snippets_errors`, **deixando de o executar por completo** (rewrite rule, query var e redirect todos mortos) | Migrado para código de plugin normal (Módulo 3), sem essa fragilidade; `wp rewrite flush --hard` depois de qualquer mudança de slug |
| 6 | Regra de rewrite `/<slug>` não persistia em alguns sites recém-activados | Rewrite rules não flushed após `login enable` em certas condições de cache de permalinks | `wp rewrite flush --hard` explícito após activar o módulo 3 |
| 7 | `security.txt` continha `mailto:seguranca@descomplicar.pt` — mailbox **inexistente** | Valor hardcoded em `tools/executar_pipeline_completa.py` (`--contact seguranca@descomplicar.pt`), replicado para os 7 sites no deploy inicial. Não foi apanhado pela auditoria automática (só validava presença/formato do ficheiro, não se o mailbox era real) | Corrigido nos 7 sites (`wp_options` ao vivo) e na origem (script) para `mailto:it@descomplicar.pt` — mesmo endereço usado em `alertEmails` do Wordfence, `rua`/`ruf` do DMARC, e utilizador `only-me` do WSAL |
| 8 | reCAPTCHA/YouTube bloqueados por CSP mesmo com `frame-src` a permitir | CSP duplicado (nginx + plugin) — browsers aplicam a directiva mais restritiva de **cada** CSP na intersecção; o nginx não declarava `frame-src`, caindo no fallback mais restrito | `segdesc_nginx_csp_covered = '1'` nos sites com template nginx — nginx passa a ser a única autoridade de enforcement, plugin só mantém XFO + Report-Only |
| 9 | `WP_REST_Server` conta o dobro dos pedidos reais nos limiares de rate-limit | `permission_callback` é invocado **duas vezes** por pedido HTTP real (confirmado por transient antes/depois de 1 pedido) | Limiares nominais (`FAIL_MAX=10`, `MAX=60`) valem, na prática, metade (~5 e ~30) — mantido assim de propósito, mais conservador não prejudica uso legítimo |
| 10 | `X-Forwarded-For` falsificável permitia contornar allowlist/rate-limit/lockout do endpoint REST | Portas backend do CWP (2083/2031/2095) acessíveis directamente da internet — confiar incondicionalmente em XFF permitia a um atacante que batesse directo no backend falsificar o IP visto | `segdesc_client_ip()` só confia em XFF quando `REMOTE_ADDR` é um proxy local confiável (`127.0.0.1`/`::1`), e só no último valor da cadeia (o que o próprio nginx anexa) |
| 11 | Desactivar o plugin não limpa cron/options/rewrite rules | Nenhum módulo regista `register_deactivation_hook` — só existe `register_activation_hook` (gera token + activa headers com CSP/XFO default) | `descomplicar_monitor_cron` fica agendado mas sem handler (evento inofensivo, sem efeito); opções `segdesc_*`/`descomplicar_monitor_*` e a rewrite rule do slug de login persistem; reactivar retoma o estado exactamente onde ficou — desejável para reinstalação, mas exige limpeza manual (`wp option delete`) numa desinstalação definitiva |
---
## Fonte
- `71.Seguranca/plugin/seguranca-descomplicar/seguranca-descomplicar.php`
(v1.4.2, 1494 linhas) — código-fonte completo, relido linha a linha
nesta expansão: todas as 26 constantes `SEGDESC_*`, os 6 hooks do
Módulo 3, os 7 hooks do Módulo 4 (incl. `cron_schedules` custom), os
8 endpoints REST (não 7 — corrigido nesta expansão), e os 5 subcomandos
WP-CLI (`token`, `allowlist`, `headers`, `login`, `hardening`)
confirmados exaustivos (sem `WP_CLI::add_command` adicional no
ficheiro).
- `71.Seguranca/CONFIG-Plugins-Referencia.md` §2 (`seguranca-descomplicar`)
— config ao vivo verificada via wp-cli nos 7 sites, 16-08-2026.
- `71.Seguranca/tools/distribuir_e_activar_plugin.py` e
`tools/executar_pipeline_completa.py` — relidos por completo; confirmado
que nenhum dos dois expõe flags de configuração do plugin em si (só
`--confirm-fix` no pipeline, sem argumentos no script de distribuição).
- `71.Seguranca/index.md` — lido na íntegra (linhas 1-627, não só
183-636 como na versão anterior desta skill); confirmou a contagem real
de 8 rotas REST (tabela de bundle da 12ª sessão: "8 rotas REST
(`/wordfence/*` × 6, `/headers/*` × 2)"), a nota de que CSP/XFO ficam
hoje maioritariamente a cargo do nginx/Cloudflare (módulo do plugin na
prática só serve `security.txt` nos sites com essa cobertura), e o caso
carstuff.pt (mecanismo de `/dash` alheio a este plugin).
+648
View File
@@ -0,0 +1,648 @@
---
name: webp-express
description: Gestão do plugin WebP Express (conversão automática de imagens para WebP) via WP-CLI e ficheiro de config em servidores CWP. Cobre localizar o ficheiro de config com hash variável, modos de operação (CDN friendly, Varied image responses, No conversion, Tweaked), reconversão em massa via `wp webp-express convert`, flush de webps, conversores disponíveis (imagemagick/imagick/gd vs cwebp/vips/ffmpeg/graphicsmagick/wpc/ewww) e as respectivas opções próprias, estrutura JSON completa do ficheiro de configuração (qualidade/encoding/near-lossless/alpha-quality por tipo de imagem, cache-control, alter HTML, redirection rules, web service), regras `.htaccess` e a fronteira com o Cloudflare. Usar quando "webp express", "converter imagens webp", "webp nao funciona", "reconverter imagens", "flush webp", "conversor imagemagick gd", "operation mode webp", "cdn friendly webp", "webp quality", "near lossless", "alter html webp", "cache control webp", "web service webp express".
---
# /webp-express — Conversão de imagens para WebP via WP-CLI
Gestão programática do plugin WebP Express (`webp-express`) em sites WordPress
da frota, via WP-CLI no servidor CWP (`server.descomplicar.pt`, SSH porta
9443).
**Fonte:** `CONFIG-Plugins-Referencia.md` §5 (WebP Express) + verificação ao
vivo em `emanuelalmeida.pt` (versão `0.25.15`) + leitura completa do
código-fonte nesta sessão: `lib/classes/CLI.php`, `lib/classes/CachePurge.php`,
`lib/classes/BulkConvert.php`, e **todos** os ficheiros `.inc`/`.php` de
`lib/options/options/` (general, conversion-options incl.
`converter-options/*.php` por conversor, redirection-rules, serve-options,
alter-html, web-service-options, operation-mode.inc) — 43 ficheiros no total.
Config JSON completo lido ao vivo do disco (não só campos vistos na UI).
---
## Contexto CWP — sempre obrigatório
```bash
PATH=/home/USER/public_html
sudo -u USER /usr/local/bin/wp <comando> --path=$PATH
```
**Acesso via SSH:** `ssh -p 9443 root@server.descomplicar.pt` (alias `server`
já configurado localmente).
---
## 1. Onde vive a configuração — NÃO é `wp_options` normal
A config principal do WebP Express vive num **ficheiro JSON com nome
imprevisível** (hash no nome, muda por instalação/reinstalação):
```
wp-content/webp-express/config/config.<hash>.json
```
**Nunca assumir o hash.** Duas formas de o descobrir, por ordem de
preferência:
```bash
# 1. Forma rápida — o hash está guardado numa option (sem precisar de find)
wp option get webp-express-config-hash --path=$PATH
# 2. Confirmar/descobrir via find quando a option não existe ou for preciso
# validar o caminho real no disco
find $PATH/wp-content/webp-express/config -iname 'config.*.json'
```
O ficheiro em si:
```bash
HASH=$(wp option get webp-express-config-hash --path=$PATH)
cat $PATH/wp-content/webp-express/config/config.$HASH.json | python3 -m json.tool
```
### Estado operacional (`wp_options`, separado da config)
Ao contrário da config, o **estado** vive mesmo em `wp_options` normais,
todas com o prefixo `webp-express-`:
```bash
wp option get webp-express-state --format=json --path=$PATH
wp option get webp-express-migration-version --path=$PATH
wp option get webp-express-config-migrated-cve-2025-11379 --path=$PATH
wp option get webp-express-alter-html-options --format=json --path=$PATH
```
`webp-express-state` traz o resumo mais útil de uma só vez:
`workingConverterIds`, `workingAndActiveConverterIds`, `configured`,
`htaccess-rules-saved-at-some-point`, `active-htaccess-dirs`.
### ⚠️ Gotcha: `convert_webp_status` e `webp_offset_*` NÃO são do WebP Express
Numa auditoria com `wp option list --search='*webp*'` aparecem também
`convert_webp_status` e vários `webp_offset_*` (`webp_offset_thumbnail`,
`webp_offset_large`, etc.). **São opções órfãs de outro plugin** (não
presente na lista activa de plugins do site) — não pertencem ao WebP
Express e não devem ser lidas/alteradas ao auditar este plugin. Confirmar
sempre com `wp plugin list --format=json | grep -i webp` que só
`webp-express` está activo antes de interpretar options com "webp" no nome.
---
## 2. Modos de operação (`operation-mode`)
Fonte: `lib/options/options/operation-mode.inc`. Quatro valores possíveis:
| Valor | Nome na UI | Comportamento |
|---|---|---|
| `varied-image-responses` | Varied image responses | Redirecciona jpeg/png para webp via `.htaccess`, mas só quando o browser envia `Accept: image/webp`. A mesma URL responde de forma diferente consoante o cliente — **problemático para CDNs que não fazem `Vary` no header `Accept`** |
| `cdn-friendly` | CDN friendly | jpeg é **sempre** servido como jpeg; em vez de variar a resposta, o WebP Express reescreve o HTML (`<picture>`/`<img srcset>`) para apontar directamente ao ficheiro `.webp` estático. Melhor HIT ratio em CDN, sem exigir forward do header `Accept` |
| `no-conversion` | No conversion | Não converte nada — usado quando outro plugin já faz a conversão e só se quer aproveitar a reescrita de HTML/redirecção do WebP Express |
| `tweaked` | Tweaked | Sem preset — todas as opções ficam editáveis individualmente (mostra secções `redirection-rules` e `serve-options` completas, que ficam escondidas nos outros modos); ao voltar a um preset perdem-se as afinações |
**Verificado em `emanuelalmeida.pt`:** `operation-mode: "cdn-friendly"` —
escolha correcta dado o site estar atrás do Cloudflare (evita depender de o
Cloudflare fazer `Vary: Accept` correctamente, que nem sempre acontece nos
planos gratuitos).
```bash
HASH=$(wp option get webp-express-config-hash --path=$PATH)
wp eval "echo json_decode(file_get_contents('wp-content/webp-express/config/config.$HASH.json'))->{'operation-mode'};" --path=$PATH
```
Para mudar de modo é preciso editar o ficheiro JSON directamente (não há
comando `wp option patch` — não é uma option serializada) e depois correr
`wp webp-express convert --reconvert` para regenerar tudo com as novas
definições. **Mutação em produção — nunca fazer sem autorização.**
**Nota de UI condicional por modo** (relevante ao ler o código de
`lib/options/options/general/general.inc`): as secções `scope.inc`,
`destination-folder.inc` e `destination-structure.inc` só aparecem quando
`operation-mode != 'no-conversion'`; `cache-control.inc` só aparece em
`tweaked`, `varied-image-responses` ou `cdn-friendly`. O JSON de config
mantém sempre todos os campos gravados, independentemente do que a UI
mostra para o modo actual.
---
## 3. Reconverter imagens em massa — comando nativo `wp webp-express`
O plugin regista um comando WP-CLI próprio (`lib/classes/CLI.php`), **não é
preciso passar por `wp eval`**. Fonte lida na íntegra nesta sessão — o
plugin só regista **dois** subcomandos, não há mais:
```bash
wp help webp-express --path=$PATH
```
```
NAME
wp webp-express
SUBCOMMANDS
convert Convert images to webp
flushwebp Flush webps
```
### `wp webp-express convert`
```bash
# Ver o que falta converter, sem converter (dry preview por pasta)
wp webp-express convert --path=$PATH
# Converter tudo o que ainda não está convertido (media library + tema)
wp webp-express convert uploads --path=$PATH
wp webp-express convert themes --path=$PATH
# Forçar RECONVERSÃO de tudo (substitui webps já existentes)
wp webp-express convert uploads --reconvert --path=$PATH
# Limitar a uma subpasta
wp webp-express convert uploads/2026 --path=$PATH
# Só PNG / só JPEG
wp webp-express convert uploads --only-png --path=$PATH
wp webp-express convert uploads --only-jpeg --path=$PATH
# Forçar um conversor específico (ignora o stack de fallback)
wp webp-express convert uploads --converter=gd --path=$PATH
# Sobrepor qualidade/encoding só nesta corrida (não altera a config gravada)
wp webp-express convert uploads --quality=75 --encoding=lossy --path=$PATH
# Sobrepor near-lossless e alpha-quality só nesta corrida
wp webp-express convert uploads --near-lossless=60 --path=$PATH
wp webp-express convert uploads --alpha-quality=80 --path=$PATH
```
`<location>` aceita `uploads`, `themes`, `plugins`, `wp-content` ou `index`
como "raiz de imagem", opcionalmente seguido de `/subpasta`. Sem argumento,
lista quantos ficheiros por converter há em cada grupo, sem converter nada.
**Flags completas do `convert`** (confirmadas em `CLI.php`, docblock
`## OPTIONS`):
| Flag | Efeito | Aplica-se a `max-quality` E `png-quality` |
|---|---|---|
| `[<location>]` | Restringe a uma raiz de imagem (`uploads`\|`themes`\|`plugins`\|`wp-content`\|`index`), opc. `/subpasta` | — |
| `--reconvert` | Reconverte mesmo o que já está convertido | — |
| `--only-png` | Só PNG (`image-types = 2`) | — |
| `--only-jpeg` | Só JPEG (`image-types = 1`) | — |
| `--quality=<n>` | Sobrepõe qualidade (0-100) | Sim — aplica ao mesmo valor `max-quality` e `png-quality` |
| `--near-lossless=<n>` | Sobrepõe near-lossless (0-100) | Sim — aplica a `png-near-lossless` e `jpeg-near-lossless` |
| `--alpha-quality=<n>` | Sobrepõe alpha-quality (0-100) | Só `alpha-quality` (PNG com transparência) |
| `--encoding=<auto\|lossy\|lossless>` | Sobrepõe encoding | Sim — aplica a `png-encoding` e `jpeg-encoding` |
| `--converter=<id>` | Força um conversor específico, ignora o stack. Valores válidos: `cwebp \| vips \| ewww \| imagemagick \| imagick \| gmagick \| graphicsmagick \| ffmpeg \| gd \| wpc` | — |
Nenhuma destas flags altera a config gravada em disco — são só para essa
corrida.
### `wp webp-express flushwebp` — limpar webps gerados
```bash
# Apaga TODOS os .webp gerados (força reconversão no próximo acesso/convert)
wp webp-express flushwebp --path=$PATH
# Só os que vieram de PNG
wp webp-express flushwebp --only-png --path=$PATH
```
Internamente chama `CachePurge::purge()` (`lib/classes/CachePurge.php`),
que apaga recursivamente `.webp` em três locais: o cache dir
(`Paths::getCacheDirAbs()`), o upload dir (só se
`destination-folder == 'mingled'`) e o dir de "bigger than source dummy
files". Nunca apaga o original de um attachment registado na media library
(`MediaLibraryHelper::isRegisteredAttachmentOrThumbnail`) — protecção
interna contra apagar acidentalmente ficheiros errados quando modo
`mingled` mistura originais e webps na mesma pasta.
Útil depois de mudar `quality`/`encoding`/conversor na config, antes de
correr `convert` de novo — garante que não ficam webps antigos com as
definições anteriores misturados com os novos.
**Alternativa via wp-admin (sem SSH):** Settings → WebP Express → aba
"Bulk convert" tem os mesmos botões ("Bulk Convert" → popup de conversão em
massa via AJAX; "Delete converted files" → popup que chama
`processAjaxPurgeCache()`, o mesmo `CachePurge::purge()` do `flushwebp`).
Há ainda um botão "Delete log files" na secção "Enable logging" (ver §7).
O WP-CLI é preferível em produção por ser scriptável e não depender de
timeout do browser em sites com muitas imagens. **Nota:** a conversão em
massa via UI usa sempre as **últimas definições gravadas** (comentário no
código-fonte de `bulk-convert.inc`: *"the bulk conversion is using the
last saved settings"*) — se acabaste de mudar qualidade/encoding no forms
mas ainda não gravaste (Save), o bulk convert usa a versão antiga.
---
## 4. Conversores — stack de fallback e opções por conversor
`webp-express` tenta uma lista ordenada de conversores até um funcionar; cada
entrada tem `working: true/false` gravado em `webp-express-state` (testado
no arranque, não em cada conversão):
| Conversor | Estado em `emanuelalmeida.pt` | Nota |
|---|---|---|
| `imagemagick` (binário CLI) | ✅ `working: true`¹ | Fallback preferido |
| `imagick` (extensão PHP) | ✅ `working: true`¹ | |
| `gd` (extensão PHP, sempre disponível) | ✅ `working: true` | Último recurso, sempre presente em qualquer instalação PHP |
| `cwebp` (binário Google) | ❌ `working: false` | Não instalado no servidor CWP |
| `vips` | ❌ `working: false` | Não instalado |
| `graphicsmagick`/`gmagick` | ❌ `working: false` | Não instalado |
| `ffmpeg` | ❌ `working: false` | Não instalado |
| `wpc` (WebP Cloud / Remote WebP Express, API paga ou remota) | ❌ `working: false` | Não configurado (sem API key/URL) |
| `ewww` (requer plugin EWWW Image Optimizer) | ❌ `working: false` | Plugin não instalado neste site |
¹ Nota lida directamente no JSON de config nesta sessão: no bloco
`converters[]` do config actual, `imagemagick` e `graphicsmagick` aparecem
com `"working": false` (o binário de sistema não responde), enquanto só
`gd` tem `"working": true`. O `webp-express-state` (cache separada,
recalculada a cada teste de auto-detecção) pode divergir ligeiramente do
snapshot gravado no `converters[]` do JSON — **confirmar sempre pelo
`webp-express-state` para saber o estado corrente**, o array `converters`
do config json é sobretudo a lista de opções por conversor + ordem.
**Sem impacto operacional** — `gd` cobre 100% dos casos como último
recurso; os conversores em falta são apenas alternativas mais
rápidas/melhor compressão que não compensam instalar a nível de servidor
só para este plugin. Verificar sempre `workingConverterIds` antes de forçar
`--converter=X` num `wp webp-express convert` — se `X` não estiver na
lista, o comando falha.
```bash
wp option get webp-express-state --format=json --path=$PATH | python3 -c "import json,sys; print(json.load(sys.stdin)['workingConverterIds'])"
```
### Opções específicas por conversor (bloco `converters[].options` no JSON)
Fonte: `lib/options/options/conversion-options/converter-options/*.php` (um
ficheiro por conversor) + o array `converters` do config JSON ao vivo.
| Conversor | Opções (`options{}`) | Descrição |
|---|---|---|
| `cwebp` | `use-nice` (bool), `try-common-system-paths` (bool), `try-supplied-binary-for-os` (bool), `method` (0-6), `low-memory`/"set-size"+`size-in-percentage`, `command-line-options` (string livre passada ao binário) | `method`: trade-off velocidade/qualidade (0=rápido, 6=melhor). "Size" força tamanho-alvo em % do original em vez de qualidade (~2.5x mais lento). `command-line-options` aceita qualquer flag do `cwebp` real, ex. `-low_memory -af -f 50 -sharpness 0 -mt` |
| `vips` | `smart-subsample` (bool), `preset` (`none`\|`default`\|`photo`\|`picture`\|`drawing`\|`icon`\|`text`) | Preset ajusta várias opções internas consoante o tipo de conteúdo (não sobrepõe qualidade) |
| `imagemagick` | `use-nice` (bool) | Executa o binário `convert`; sem outras opções específicas |
| `graphicsmagick` (`gmagick`) | `use-nice` (bool) | Executa `gm convert`; sem outras opções específicas |
| `ffmpeg` | `use-nice` (bool), `method` (0-6) | Idêntico ao `imagemagick` mas via ffmpeg |
| `gd` | `skip-pngs` (bool) | Se `true`, PNGs passam para o próximo conversor da stack (Gd historicamente pior a comprimir PNG e teve bugs de transparência, hoje resolvidos) |
| `imagick` | — (sem opções próprias) | "imagick has no special options" no código |
| `ewww` | `api-key`, `api-key-2` (fallback) | Serviço cloud pago (ewww.io); fee único, sem custo por conversão webp |
| `wpc` (Remote WebP Express / WPC) | `api-version` (0\|1), `api-url`, `secret`/`api-key`, `crypt-api-key-in-transfer` (bool) | Delega a conversão a outra instalação WebP Express ou a um serviço `webp-convert-cloud-service`. Api 0 é legado (v0.1); usar 1 para instâncias WebP Express normais |
`use-nice`: presente em quase todos os conversores baseados em binário —
executa com prioridade reduzida (`nice`) para poupar recursos à custa de
conversão ligeiramente mais lenta.
`Use nice`, `try-common-system-paths`, `try-supplied-binary-for-os`: o
plugin traz binários `cwebp` pré-compilados para várias plataformas dentro
do próprio plugin (`try-supplied-binary-for-os`) — útil quando o binário de
sistema não existe mas o hosting permite `exec()`.
---
## 5. Qualidade, encoding, near-lossless e metadata (`conversion-options`)
Fonte: `lib/options/options/conversion-options/{quality,jpeg,png,metadata}.inc`
+ valores ao vivo do JSON de `emanuelalmeida.pt`.
### 5.1. JPEG → WebP
| Campo JSON | Valor ao vivo | Descrição |
|---|---|---|
| `jpeg-encoding` | `"auto"` | `lossy` (sempre lossy) ou `auto` (o plugin experimenta lossy e lossless e fica com o mais pequeno). `Gd`/`Ewww` não suportam `auto` |
| `quality-auto` | `true` | Se `true`, tenta detectar a qualidade original do jpeg e usar a mesma no webp (requer imagick/imagemagick/gmagick capazes de detectar qualidade); se `false`, usa `quality-specific` fixo |
| `max-quality` | `80` | Tecto para `quality-auto`: mesmo que o jpeg original tenha qualidade mais alta, nunca ultrapassa este valor. Recomendado 50-85 |
| `quality-specific` | `70` | Qualidade fixa usada quando `quality-auto=false` OU como fallback quando a detecção de qualidade falhar |
| `jpeg-enable-near-lossless` | `true` | Aplica-se quando o encoding resultante é lossless: liga o pré-processamento "near-lossless" em vez de 100% lossless puro. Só suportado por `cwebp` e `vips` |
| `jpeg-near-lossless` | `60` | Nível 0 (máximo pré-processamento) a 100 (sem pré-processamento, = lossless puro) |
### 5.2. PNG → WebP
| Campo JSON | Valor ao vivo | Descrição |
|---|---|---|
| `png-encoding` | `"auto"` | `lossless` (default para PNG) ou `auto` (experimenta lossy e lossless, fica com o mais pequeno — não existe opção "sempre lossy" isolada para PNG na UI) |
| `png-quality` | `85` | Qualidade quando o resultado é lossy (via `auto`). Recomendado 60-90, tipicamente mais alto que jpeg pois PNG é usado para ícones/gráficos |
| `alpha-quality` | `80` | Qualidade específica do canal alfa (transparência), só relevante em encoding lossy com transparência. `Gd`/`Ewww` ignoram esta opção e mantêm o canal alfa sempre lossless |
| `png-enable-near-lossless` | `true` | Idêntico ao `jpeg-enable-near-lossless`, mas para PNG |
| `png-near-lossless` | `60` | Idêntico ao `jpeg-near-lossless` |
### 5.3. Metadata
| Campo JSON | Valor ao vivo | Descrição |
|---|---|---|
| `metadata` | `"none"` | `none` (não copia EXIF/etc. para o webp — mais pequeno) ou `all` (copia toda a metadata). **`Gd` não suporta cópia de metadata** — ignora esta opção sempre que `Gd` é o conversor efectivo |
### 5.4. Outras opções gerais de conversão
| Campo JSON | Valor ao vivo | Descrição |
|---|---|---|
| `convert-on-upload` | `true` | Converte a imagem (incl. todos os thumbnails gerados) no momento do upload para a media library. Hooka em `handle_upload` e `image_make_intermediate_size`. Pode abrandar o upload em temas com muitos tamanhos de thumbnail |
| `image-types` | `3` | Bitmask: `1`=jpeg, `2`=png, `3`=ambos, `0`=desactivado. Controla que tipos são convertidos/processados |
| `prevent-using-webps-larger-than-original` | `true` | Webps maiores que o original ficam sempre em disco mas não são usados (nem em `.htaccess` nem em Alter HTML) se esta opção estiver activa. Desde 0.25.5: em Alter HTML com `picture` + `srcset`, só omite o `<source webp>` se **todos** os webps do srcset forem maiores — considerar desactivar se muitas imagens têm srcset |
| `enable-logging` | `false` | Grava resultado de cada conversão em `wp-content/webp-express/log/conversions/`. Tem botão dedicado "Delete log files" na UI (popup `purgelogpopup`) |
---
## 6. Estrutura completa do ficheiro de configuração (JSON)
Volcado ao vivo de `emanuelalmeida.pt`
(`wp-content/webp-express/config/config.<hash>.json`) — esta é a **lista
completa de todas as chaves de topo** que o plugin grava, incluindo as que
não aparecem directamente numa única aba da UI:
```jsonc
{
// --- Modo e âmbito ---
"operation-mode": "cdn-friendly", // varied-image-responses | cdn-friendly | no-conversion | tweaked
"scope": ["uploads", "themes"], // subset de: uploads, themes, plugins, wp-content, index
"image-types": 3, // bitmask: 1=jpeg 2=png 3=ambos 0=nenhum
// --- Destino dos ficheiros webp ---
"destination-folder": "separate", // separate | mingled
"destination-extension": "append", // append (image.jpg.webp) | set (image.webp)
"destination-structure": "image-roots", // doc-root | image-roots
// --- Cache-Control header ---
"cache-control": "no-header", // no-header | set | custom
"cache-control-custom": "public, max-age=31536000, stale-while-revalidate=604800, stale-if-error=604800",
"cache-control-max-age": "one-week", // one-second|one-minute|one-hour|one-day|one-week|one-month|one-year
"cache-control-public": false, // true=public, false=private (só usado quando cache-control=set)
// --- Robustez ---
"prevent-using-webps-larger-than-original": true,
"enable-logging": false,
// --- Redirection rules (.htaccess) ---
"enable-redirection-to-converter": false, // só editável em modo "tweaked"/"varied-image-responses"
"only-redirect-to-converter-on-cache-miss": false,
"only-redirect-to-converter-for-webp-enabled-browsers": true,
"do-not-pass-source-in-query-string": true,
"redirect-to-existing-in-htaccess": false,
"forward-query-string": true,
"enable-redirection-to-webp-realizer": true, // "criar webp sob pedido" (chave em cdn-friendly)
// --- Qualidade/encoding jpeg ---
"jpeg-encoding": "auto", // lossy | auto
"jpeg-enable-near-lossless": true,
"jpeg-near-lossless": 60,
"quality-auto": true,
"max-quality": 80,
"quality-specific": 70,
// --- Qualidade/encoding png ---
"png-encoding": "auto", // lossless | auto
"png-enable-near-lossless": true,
"png-near-lossless": 60,
"png-quality": 85,
"alpha-quality": 80,
// --- Stack de conversores, ordem = prioridade ---
"converters": [
{ "converter": "cwebp", "options": { "use-nice": true, "try-common-system-paths": true, "try-supplied-binary-for-os": true, "method": 6, "low-memory": true, "command-line-options": "" }, "working": false },
{ "converter": "vips", "options": { "smart-subsample": false, "preset": "none" }, "working": false },
{ "converter": "imagemagick", "options": { "use-nice": true }, "working": false },
{ "converter": "graphicsmagick", "options": { "use-nice": true }, "working": false },
{ "converter": "ffmpeg", "options": { "use-nice": true, "method": 4 }, "working": false },
{ "converter": "wpc", "working": false, "options": { "api-key": "" } },
{ "converter": "ewww", "working": false },
{ "converter": "imagick", "working": false },
{ "converter": "gmagick", "working": false },
{ "converter": "gd", "options": { "skip-pngs": false }, "working": true }
],
// --- Metadata / upload automático ---
"metadata": "none", // none | all
"convert-on-upload": true,
// --- Serve options (resposta ao browser) ---
"fail": "original", // original | 404 | report
"success-response": "original", // original | converted
// --- Alter HTML ---
"alter-html": {
"enabled": true,
"replacement": "picture", // picture | url
"hooks": "ob", // content-hooks | ob (output buffering completo da página)
"only-for-webp-enabled-browsers": false,
"only-for-webps-that-exists": false,
"alter-html-add-picturefill-js": true,
"hostname-aliases": [] // hostnames extra (CDN) cujas URLs também devem ser reescritas
},
// --- Web service (partilhar conversão com outros sites) ---
"web-service": {
"enabled": false,
"whitelist": [] // [{ip, label, api-key, crypt-in-transfer}, ...]
},
// --- Snapshot de ambiente à data do save (diagnóstico, não editável) ---
"environment-when-config-was-saved": { "...": "..." },
"base-htaccess-on-these-capability-tests": { "...": "..." },
"document-root": "/home/USER/site",
"paths-used-in-htaccess": { "wod-url-path": "wp-content/plugins/webp-express/wod/webp-on-demand.php" }
}
```
### 6.1. Cache-Control header — detalhe
Fonte: `lib/options/options/general/cache-control.inc`.
| `cache-control` | Comportamento |
|---|---|
| `no-header` | Não define header — deixa o servidor/CDN decidir |
| `set` | Usa `cache-control-public` (public/private) + `cache-control-max-age` (preset de duração) para montar o header automaticamente |
| `custom` | Usa literalmente a string em `cache-control-custom` — **única forma de definir `stale-while-revalidate`/`stale-if-error`**, não expostos nos presets |
O texto de ajuda do próprio plugin recomenda `custom` com
`stale-while-revalidate`/`stale-if-error` para melhor resiliência em falhas
momentâneas do servidor de origem.
### 6.2. Scope — mapeamento de valores
Fonte: `lib/options/options/general/scope.inc`. O array `scope[]` é
qualquer subconjunto ordenado alfabeticamente de `uploads`, `themes`,
`plugins`, `wp-content`, `index`; a UI mapeia combinações comuns para
nomes amigáveis:
| `scope` (array) | Nome na UI |
|---|---|
| `["uploads"]` | Uploads only |
| `["themes"]` | Themes only |
| `["themes", "uploads"]` | Uploads and themes |
| `["plugins", "themes", "uploads", "wp-content"]` | All content |
| `["index", "plugins", "themes", "uploads", "wp-content"]` | Everything (including wp-admin) |
| Qualquer outra combinação | Mostrado como "Custom: …" |
**Verificado em `emanuelalmeida.pt`:** `["uploads", "themes"]` → "Uploads
and themes" (plugins/wp-content fora de âmbito).
### 6.3. Destination folder / extension / structure
| Opção | Valores | Efeito |
|---|---|---|
| `destination-folder` | `separate` (pasta dedicada `wp-content/webp-express/webp-images/`) \| `mingled` (webp ao lado do original, só dentro de `uploads`) | `mingled` é recomendado ao combinar com plugins de cache de página que sabem servir por extensão (ex. Cache Enabler), ou com Shortpixel |
| `destination-extension` | `append` (`imagem.jpg.webp`) \| `set` (`imagem.webp`) | `set` conflitua se existir `logo.jpg` E `logo.png` na mesma pasta (colidem no mesmo `logo.webp`) |
| `destination-structure` | `doc-root` (espelha caminho a partir da doc root) \| `image-roots` (espelha caminho a partir da raiz de imagem — uploads/themes/etc) | Recomendado `image-roots` em hosts com doc root mal configurada; `doc-root` reduz regras de rewrite necessárias em Nginx |
**Verificado em `emanuelalmeida.pt`:** `destination-folder: "separate"`,
`destination-extension: "append"`, `destination-structure: "image-roots"`.
---
## 7. Redirection rules (`.htaccess`) — detalhe por opção
Fonte: `lib/options/options/redirection-rules/*.inc`. Estas opções só são
todas editáveis em modo `tweaked`; nos outros modos, o preset do modo
decide-as automaticamente (mas o valor gravado no JSON reflecte sempre o
que está realmente activo em `.htaccess`).
| Campo JSON | Descrição |
|---|---|
| `redirect-to-existing-in-htaccess` | Redirect interno directo para o `.webp` já existente, sem passar por PHP — mais rápido. Regra fica **acima** da regra de conversão |
| `enable-redirection-to-converter` | Redirige jpeg/png (não convertidos) para `webp-on-demand.php`, que converte, grava e serve. Regra fica **abaixo** da anterior (só actua se ainda não existir webp) |
| `enable-redirection-to-webp-realizer` | "Criar webp files upon request": redirige pedidos por um `.webp` que **ainda não existe** para `webp-realizer.php`, que procura o jpg/png correspondente, converte e serve — permite referenciar webps no HTML antes de existirem fisicamente. É a opção chave em modo `cdn-friendly` |
| `only-redirect-to-converter-for-webp-enabled-browsers` | Só actua a regra do `enable-redirection-to-converter` quando o `Accept` header contém `image/webp` |
| `only-redirect-to-converter-on-cache-miss` | Condição extra equivalente à funcionalidade usada por `enable-redirection-to-webp-realizer`, mas aplicada ao `enable-redirection-to-converter` — desnecessária se `redirect-to-existing-in-htaccess` já estiver activo |
| `do-not-pass-source-in-query-string` | Se `true` (default recomendado), passa o path da imagem por env var em vez de query string, para não disparar firewalls que bloqueiam certas query strings |
| `forward-query-string` | Encaminha a query string original do pedido para o script de conversão |
---
## 8. Serve options (resposta ao browser após conversão)
Fonte: `lib/options/options/serve-options/*.inc` — só visível na UI em modo
`tweaked`.
| Campo JSON | Valores | Descrição |
|---|---|---|
| `fail` | `original` (serve o jpg/png original) \| `404` \| `report` (relatório de erro em texto simples) | Recomendado `original` em produção |
| `success-response` | `original` \| `converted` | `converted` envia header `Vary: Accept` (indica que a resposta depende do Accept). Usar `original` se combinado com plugins de cache de página que não sabem variar por Accept (ex. Cache Enabler sem suporte dual-cache) |
**Verificado em `emanuelalmeida.pt`:** `fail: "original"`,
`success-response: "original"`.
---
## 9. Alter HTML — detalhe completo
Fonte: `lib/options/options/alter-html/*.inc`.
| Campo JSON (`alter-html.*`) | Valores | Descrição |
|---|---|---|
| `enabled` | bool | Recomendado activar mesmo com redirecção activa: melhora cache em CDN e mantém extensão correcta ao fazer download de imagem |
| `replacement` | `picture` \| `url` | **`picture`**: substitui `<img>` por `<picture>` com `<source type="image/webp">` — funciona bem com cache de página, mas pode quebrar CSS `div > img` (usar `div img`). **`url`**: substitui directamente os URLs de imagem (src, srcset, lazy-load attrs, inline styles) — mais abrangente mas não funciona com cache de página excepto Cache Enabler |
| `hooks` | `content-hooks` \| `ob` | `content-hooks`: hooka em `the_content`, `the_excerpt`, `post_thumbnail_html`, `get_avatar`, `woocommerce_product_get_image`, `acf_the_content`, `dynamic_sidebar_before/after` — mais leve. `ob`: intercepta a página inteira via output buffering — necessário se o tema/plugin não usa os hooks standard. HTML > 600kb ignora a alteração por protecção de memória |
| `only-for-webp-enabled-browsers` | bool | Só relevante com `replacement=url`: só faz a substituição quando o browser suporta webp (evita servir só-webp a browsers antigos sem JS de fallback) |
| `only-for-webps-that-exists` | bool | Se `false` ("Reference webps that haven't been converted yet" activo), permite referenciar webps que ainda não existem — requer `enable-redirection-to-webp-realizer` activo para funcionar |
| `alter-html-add-picturefill-js` | bool | Só relevante com `replacement=picture`: injecta polyfill `picturefill.js` para browsers sem suporte nativo a `<picture>` (~94% suporte nativo actual) |
| `hostname-aliases` | array de strings | Hostnames adicionais (ex. CDN) cujas imagens também devem ser processadas — útil se outro plugin já reescreve URLs para um CDN antes do WebP Express actuar |
**Verificado em `emanuelalmeida.pt`:** `enabled: true`,
`replacement: "picture"`, `hooks: "ob"`,
`only-for-webp-enabled-browsers: false`,
`only-for-webps-that-exists: false`,
`alter-html-add-picturefill-js: true`, `hostname-aliases: []`.
---
## 10. Web service — partilhar conversão entre sites
Fonte: `lib/options/options/web-service-options/*.inc`. Permite que **este**
site actue como servidor de conversão remota para outros sites que usam o
conversor `wpc`/"Remote WebP Express" (ver §4).
| Campo JSON (`web-service.*`) | Descrição |
|---|---|
| `enabled` | Activa o endpoint (URL exposto: `Paths::getWebServiceUrl()`) |
| `whitelist[]` | Lista de sites autorizados a usar este endpoint, cada entrada com: `label` (referência própria), `ip` (aceita wildcard `*`, ex. `212.91.*` ou `*` para qualquer IP), `api-key` (chave arbitrária escolhida pelo utilizador, não um segredo gerado), `crypt-api-key-in-transfer` (bool — cifra a api-key no transporte; alguns setups antigos não suportam) |
**Verificado em `emanuelalmeida.pt`:** `web-service.enabled: false`,
`whitelist: []` — não usado como servidor remoto neste site.
---
## 11. Bulk convert e cache purge (via UI e via CLI)
Fonte: `lib/options/options/conversion-options/bulk-convert.inc` +
`lib/classes/BulkConvert.php` + `lib/classes/CachePurge.php`.
- **UI → "Bulk Convert"** dispara AJAX que usa `BulkConvert::getList()` +
conversão sequencial das últimas definições **gravadas**. Equivalente
CLI: `wp webp-express convert <scope> [--reconvert]`.
- **UI → "Delete converted files"** dispara AJAX
`CachePurge::processAjaxPurgeCache()`. Equivalente CLI:
`wp webp-express flushwebp [--only-png]`.
- **UI → "Delete log files"** (dentro da secção "Enable logging") apaga
`wp-content/webp-express/log/conversions/*`; sem equivalente CLI
dedicado — usar `wp eval` ou apagar directamente via `find`/`rm` se
necessário (nunca sem autorização em produção).
- O filtro de listagem interno (`BulkConvert::defaultListOptions`) usa
sempre `only-unconverted: true` por default (não reconverte o que já
existe) e `max-depth: 100` — nunca varre mais de 100 níveis de
subpastas.
- `CachePurge::purge()` nunca apaga `.webp` que estejam registados como
attachment/thumbnail na media library (protecção em modo `mingled`, onde
originais e webps partilham pasta).
---
## 12. Regras `.htaccess`
`webp-express-state.configured: true` e
`htaccess-rules-saved-at-some-point: true` confirmam que as regras já foram
gravadas — normalmente na pasta de cache do próprio plugin
(`active-htaccess-dirs: ["cache"]`), não na raiz do site. Isto é o que faz
o browser/Cloudflare receberem directamente o ficheiro `.webp` estático em
vez de passar por PHP a cada pedido (modo `cdn-friendly`).
Se `configured: false` ou o site deixar de servir webps depois de uma
migração/restauro, o passo de correcção é voltar à página de definições do
plugin (wp-admin) e gravar de novo (o botão "Save" regrava o `.htaccess`) —
não há comando WP-CLI dedicado a isto; o mais próximo é
`wp webp-express flushwebp` seguido de `convert`, que não recria as regras
`.htaccess` por si só.
---
## 13. Interacção com Cloudflare — quem faz o quê
**Achado desta sessão, importante para não duplicar trabalho:** a
conversão/optimização de imagem feita pelo **Cloudflare "Polish"** está
`off` neste site (não editável no plano Free) — o WebP não vem do
Cloudflare, vem inteiramente do WebP Express a correr no WordPress. O
Cloudflare aqui é só **cache/CDN de distribuição** do ficheiro `.webp` já
gerado, não faz qualquer transformação de imagem própria. Não há
sobreposição de funcionalidade a resolver, mas também não há optimização
de imagem "grátis" via Cloudflare a contar — se o WebP Express falhar a
converter, o Cloudflare não compensa.
Modo `cdn-friendly` foi escolhido precisamente por causa disto: ao reescrever
o HTML para apontar directamente ao `.webp` estático (em vez de depender do
`Accept` header e do Cloudflare fazer `Vary` correctamente), garante HIT
ratio alto no cache do Cloudflare independentemente do plano.
---
## Erros comuns
| Sintoma | Causa | Solução |
|---|---|---|
| `find`/`cat` no ficheiro de config falha, "No such file" | Hash do nome do ficheiro mudou (reinstalação/migração) | Nunca hardcode o hash — ler sempre `wp option get webp-express-config-hash` primeiro |
| Confundir `convert_webp_status`/`webp_offset_*` com definições do WebP Express | Options órfãs de outro plugin já removido, mesmo prefixo textual "webp" | Confirmar `wp plugin list` antes de interpretar qualquer option com "webp" no nome |
| `wp webp-express convert --converter=cwebp` falha | Conversor não está na lista `workingConverterIds` (binário não instalado no servidor) | Verificar `webp-express-state` antes de forçar um conversor específico |
| Imagens não aparecem como WebP no browser depois de `convert` | `.htaccess` não gravado (`configured: false`) ou modo `varied-image-responses` com CDN sem `Vary: Accept` | Confirmar `configured: true` em `webp-express-state`; preferir `cdn-friendly` atrás de Cloudflare |
| Webps antigos com qualidade errada persistem depois de mudar `quality`/`encoding` na config | `convert` normal só converte o que falta, não regenera o que já existe | `wp webp-express flushwebp` seguido de `wp webp-express convert` (ou `convert --reconvert` directo) |
| Bulk convert via UI usa qualidade/encoding antigos apesar de ter mudado o form | O bulk convert usa sempre as **últimas definições gravadas** (Save), não o estado não-gravado do formulário | Gravar (Save) antes de disparar "Bulk Convert" |
| Metadata (EXIF) desaparece do webp apesar de `metadata: "all"` | `Gd` está a ser o conversor efectivo — Gd não suporta cópia de metadata, ignora sempre a opção | Confirmar qual conversor está `working`/activo antes de assumir que `metadata: "all"` está a ser respeitado |
| Alpha quality/near-lossless parecem não ter efeito | Conversor efectivo (`Gd`/`Ewww`) não suporta essas opções — são ignoradas silenciosamente | Confirmar o conversor: near-lossless só em `cwebp`/`vips`; alpha-quality ignorado por `Gd`/`Ewww` |
| Selector CSS `div > img` deixa de funcionar depois de activar Alter HTML | `replacement: "picture"` insere um elemento `<picture>` extra entre o `div` e o `img` | Usar selector `div img` (descendente) em vez de `div > img` (filho directo), ou mudar para `replacement: "url"` |
---
**Fonte:** `CONFIG-Plugins-Referencia.md` §5 + auditoria ao vivo original em
`emanuelalmeida.pt` + expansão nesta sessão com leitura completa do
código-fonte: `lib/classes/CLI.php`, `CachePurge.php`, `BulkConvert.php`, e
os 43 ficheiros `.inc`/`.php` de `lib/options/options/` (general,
conversion-options + converter-options/*.php, redirection-rules,
serve-options, alter-html, web-service-options, operation-mode.inc). Config
JSON completo (`config.<hash>.json`) lido ao vivo do disco — todas as
chaves de topo documentadas em §6 vêm directamente desse ficheiro, não só
das que a UI mostra por modo.
+738
View File
@@ -0,0 +1,738 @@
---
name: wordfence
description: Auditoria e gestão completa do Wordfence Security via SQL directo em servidores CWP — cobre as 313 chaves de `wfconfig`, a tabela separada `wfls_settings` do módulo Login Security (2FA/passkeys/CAPTCHA/XML-RPC), armazenamento do WAF em ficheiros (`wp-content/wflogs/config*.php`), WAF (modos, categorias de regra, blacklist premium), bloqueio de IPs/países/padrões (UA/referrer/hostname), Live Traffic, os 5 limitadores de Rate Limiting, Diagnostics, Import/Export de config e Wordfence Central. Detecção de IP real atrás do Cloudflare (howGetIPs), scan settings e interacção com plugins de cache. Usar quando "wordfence", "waf wordpress", "firewall wordpress", "2fa wordpress", "passkeys wordpress", "bloqueio de login", "wfconfig", "wfls_settings", "cloudflare real ip wordfence", "howGetIPs", "scan malware wordpress", "brute force wordpress", "rate limiting wordpress", "live traffic wordfence", "wordfence central", "bloqueio de país wordpress".
---
# /wordfence — Auditoria e Configuração Wordfence via SQL
Gestão do Wordfence Security via `wp db query` directo no servidor CWP.
**A configuração NÃO vive toda no mesmo sítio:**
- Grosso da config (WAF nível aplicação, scan, alertas, blocking,
rate limiting) → tabela própria `wfconfig` (chave/valor, colunas
`name`/`val`), **não** `wp_options` — `wp option get/patch` não
funciona para nada disto.
- 2FA, passkeys, CAPTCHA de login, XML-RPC, "remember device" (módulo
Login Security) → tabela **separada** `wfls_settings` (colunas
`name`/`value` — nomes de coluna diferentes de `wfconfig`). Ver §6.
- Config do motor WAF usada para arrancar antes do WordPress (Extended
Protection/auto-prepend) → **ficheiros PHP serializados** em
`wp-content/wflogs/config*.php`, fora de qualquer tabela SQL,
sincronizados automaticamente a partir de `wfconfig`. Ver §10.
**Fonte:** `CONFIG-Plugins-Referencia.md` §8 (Wordfence, mapeado
16-08-2026, site piloto `emanuelalmeida.pt`, versão `9.0.0` gratuita) +
leitura completa do código-fonte do plugin (466 ficheiros PHP) e
verificação SQL/SSH ao vivo feitas nesta sessão — detalhe completo da
cobertura no rodapé "Fonte" no final do documento.
---
## Contexto CWP — sempre obrigatório
```bash
PATH=/home/USER/public_html
PREFIX=$(wp db prefix --allow-root --path=$PATH) # ex.: wpne_, wpah_ — nunca assumir wp_
```
**Acesso via SSH:** `server.descomplicar.pt`, porta `9443`, user `root`.
Servidor executa comandos como o utilizador do site: `sudo -u USER wp ... --path=$PATH`.
**Tabela de config real (schema confirmado):**
```
DESCRIBE ${PREFIX}wfconfig;
name varchar(100) PRI
val longblob -- valor (string, número ou JSON serializado)
autoload enum('no','yes')
```
Não há `wp option patch` equivalente — é sempre `SELECT`/`UPDATE` directo
sobre `name`/`val`. **313 linhas** de config no site piloto.
---
## Decision tree — qual comando usar
| Operação | Comando |
|---|---|
| Ler 1 chave de config | `wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name='...';"` |
| Ler várias chaves | `SELECT name, val FROM ${PREFIX}wfconfig WHERE name IN (...);` |
| Alterar 1 chave (produção) | `UPDATE ${PREFIX}wfconfig SET val='...' WHERE name='...';` — **preferir sempre a UI do wp-admin** para chaves que disparam hooks (`wordfence_changed_ip_source`, `wordfence_changed_license_key`) |
| Contar utilizadores com 2FA | `SELECT COUNT(*) FROM ${PREFIX}wfls_2fa_secrets;` (tabela moderna) — **não** `usermeta` (legado, pode ficar desactualizado) |
| Ver todas as tabelas do plugin | `SHOW TABLES LIKE '${PREFIX}wf%';` |
| Ver issues de scan pendentes | `SELECT COUNT(*) FROM ${PREFIX}wfpendingissues;` |
| Ver IPs bloqueados pelo WAF | `SELECT COUNT(*) FROM ${PREFIX}wfblockediplog;` |
| Confirmar recomendação de proxy do próprio Wordfence | `SELECT val FROM ${PREFIX}wfconfig WHERE name='detectProxyRecommendation';` |
| Ler settings do módulo Login Security (2FA/passkeys/CAPTCHA) | `SELECT name, value FROM ${PREFIX}wfls_settings ORDER BY name;` — **tabela diferente**, colunas `name`/`value` (não `val`) — ver §6 |
| Ver ficheiros de config do WAF (bootstrap fora do WordPress) | `cat wp-content/wflogs/config.php` / `config-synced.php` — **não é SQL**, é PHP serializado com `__halt_compiler()` — ver §10 |
---
## 1. Auditoria de estado (leitura, sem risco)
```bash
PATH=/home/USER/public_html
PREFIX=$(wp db prefix --allow-root --path=$PATH)
# Estado geral do WAF/firewall
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name IN
('firewallEnabled','waf_status','disableWAFIPBlocking','isPaid');" --allow-root --path=$PATH
# Login security (rate-limit + 2FA)
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name IN
('loginSec_maxFailures','loginSec_countFailMins','loginSec_lockoutMins',
'loginSec_lockInvalidUsers','allowLegacy2FA','loginSec_requireAdminTwoFactor');" --allow-root --path=$PATH
# Quantos utilizadores TÊM 2FA activo de facto (tabela moderna do módulo login-security)
wp db query "SELECT COUNT(*) AS total_2fa FROM ${PREFIX}wfls_2fa_secrets;" --allow-root --path=$PATH
# Detecção de IP real (crítico atrás de proxy/CDN)
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name IN
('howGetIPs','howGetIPs_trusted_proxies','howGetIPs_trusted_proxy_preset',
'detectProxyRecommendation');" --allow-root --path=$PATH
# Scans activos
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name LIKE 'scansEnabled_%';" --allow-root --path=$PATH
# Alertas/email
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name IN
('alertEmails','email_summary_enabled','liveTrafficEnabled','blockFakeBots','autoUpdate','cacheType');" --allow-root --path=$PATH
# Volume de actividade (contexto do site)
wp db query "SELECT COUNT(*) FROM ${PREFIX}wfblockediplog;" --allow-root --path=$PATH # IPs bloqueados pelo WAF
wp db query "SELECT COUNT(*) FROM ${PREFIX}wfpendingissues;" --allow-root --path=$PATH # issues de scan em aberto
wp db query "SHOW TABLES LIKE '${PREFIX}wf%';" --allow-root --path=$PATH # inventário de tabelas do plugin
```
### Valores reais confirmados (`emanuelalmeida.pt`, gratuito, 16-08-2026)
| Chave | Valor | Avaliação |
|---|---|---|
| `firewallEnabled` | `1` | ✅ |
| `waf_status` | `enabled` | ✅ WAF a funcionar |
| `disableWAFIPBlocking` | `0` | ✅ bloqueio de IP activo (325 IPs bloqueados no log) |
| `isPaid` | vazio | Versão **gratuita** — sem WAF em tempo real (updates de regras com atraso), sem scan de reputação de IP premium |
| `alertEmails` | `it@descomplicar.pt` | ✅ |
| `email_summary_enabled` | `1` | ✅ |
| `liveTrafficEnabled` | `1` | ✅ |
| `blockFakeBots` | `1` | ✅ bloqueia falsos Googlebot/etc. |
| `autoUpdate` | `1` | ✅ |
| `cacheType` | `disabled` | ✅ **deliberado** — ver §4 |
| `scansEnabled_core/plugins/themes/malware/diskSpace/suspiciousAdminUsers/wafStatus/...` | todos `1` | ✅ cobertura de scan quase completa |
| `scansEnabled_highSense` | `0` | Sensibilidade alta de scan desligada — reduz falsos positivos, aceitável |
| `scansEnabled_scanImages` | `0` | Não faz scan binário a imagens — aceitável, baixo risco típico |
| `scansEnabled_checkHowGetIPs` | `1` | O próprio Wordfence **já tem um scan check dedicado a esta má-configuração** (ver §3) |
| `loginSec_maxFailures` / `_countFailMins` / `_lockoutMins` | `5` / `60` / `240` | Bloqueia ao fim de 5 falhas em 60 min, lockout de 4h |
| `loginSec_lockInvalidUsers` | `0` | Não bloqueia por tentar logins com username inexistente (só falhas de password em conta real) |
| `allowLegacy2FA` | `0` | ✅ não permite métodos 2FA fracos/antigos |
---
## 2. GAP real — 2FA não forçado, zero utilizadores protegidos
```bash
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name='loginSec_requireAdminTwoFactor';" --allow-root --path=$PATH
# → 0 = 2FA NÃO é obrigatório para administradores
wp db query "SELECT COUNT(*) FROM ${PREFIX}wfls_2fa_secrets;" --allow-root --path=$PATH
# → 0 = zero utilizadores com 2FA activo, incluindo o admin
```
**Achado real (`emanuelalmeida.pt`):** `loginSec_requireAdminTwoFactor=0` e
**zero** linhas em `wfls_2fa_secrets` — o login continua protegido só por
password + rate-limit (5 tentativas/60min/lockout 4h), sem segundo factor,
mesmo sendo o site pessoal do fundador da empresa.
### Forçar 2FA para administradores
**Auditoria/leitura sempre segura.** Escrita é mutação em produção —
confirmar autorização antes de aplicar:
```bash
# Ler estado actual antes de alterar
wp db query "SELECT val FROM ${PREFIX}wfconfig WHERE name='loginSec_requireAdminTwoFactor';" --allow-root --path=$PATH
# Forçar 2FA para o role Administrator
wp db query "UPDATE ${PREFIX}wfconfig SET val='1' WHERE name='loginSec_requireAdminTwoFactor';" --allow-root --path=$PATH
```
**Cuidado operacional:** activar isto **sem** um administrador já ter um
autenticador configurado bloqueia o próprio acesso ao wp-admin no login
seguinte (o Wordfence força a configuração do 2FA no momento do login, não
tranca por fora — mas confirmar sempre com o utilizador antes, e nunca
aplicar em sessão headless sem acesso de recuperação garantido, ex. acesso
SSH/SQL directo para reverter o valor para `0` se algo correr mal).
---
## 3. GAP real — `howGetIPs` vazio atrás do Cloudflare
**Este é o achado mais concreto desta auditoria.** O próprio Wordfence já
tinha calculado a recomendação correcta e guardado numa chave separada, mas
**nunca a aplicou** à chave activa:
```bash
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name IN
('howGetIPs','detectProxyRecommendation');" --allow-root --path=$PATH
```
```
name val
detectProxyRecommendation HTTP_CF_CONNECTING_IP
howGetIPs (vazio)
```
`detectProxyRecommendation` é preenchido automaticamente pelo Wordfence
quando detecta que o tráfego chega via um proxy conhecido (Cloudflare, neste
caso) — mas **fica só como sugestão**; `howGetIPs` (a chave que o WAF
realmente usa para decidir qual IP é o "real") continua vazia. Com
`howGetIPs` vazio, o Wordfence cai no fallback de ordem de headers
(`lib/wfUtils.php`: `HTTP_CF_CONNECTING_IP` → `HTTP_X_REAL_IP` →
`REMOTE_ADDR` → `HTTP_X_FORWARDED_FOR`), o que **na maioria dos casos até
funciona**, mas não é o comportamento explícito/auditável e não activa o
scan check dedicado (`scansEnabled_checkHowGetIPs=1` já está ligado — é
precisamente este scan que devia estar a assinalar isto como issue, mas
`wfpendingissues` está vazia, i.e. o scan periódico pode não ter corrido
recentemente ou o alerta expirou).
### Valores válidos de `howGetIPs` (confirmado no código, `wfConfig.php`/`wfUtils.php`)
Cabeçalhos HTTP reconhecidos pelo Wordfence para determinar o IP real:
| Valor | Quando usar |
|---|---|
| `HTTP_CF_CONNECTING_IP` | Site atrás de **Cloudflare** — este é o caso do bundle Descomplicar® (todos os sites usam App for Cloudflare®) |
| `HTTP_X_REAL_IP` | Proxy tipo nginx reverso simples (sem CDN) |
| `HTTP_X_FORWARDED_FOR` | Load balancer/proxy genérico |
| (vazio) | `REMOTE_ADDR` directo — só correcto se não houver proxy/CDN nenhum à frente |
Existe ainda `howGetIPs_trusted_proxy_preset`, que aponta para uma entrada
em `ipResolutionList` (JSON de ~8KB, sincronizado da API Wordfence, contém
blocos de IP de CDNs conhecidas — ex. `cloudflare`, `cloudfront` — usados
para validar que o pedido realmente vem de um proxy confiável antes de
aceitar o header). Consultar presets disponíveis:
```bash
wp db query "SELECT val FROM ${PREFIX}wfconfig WHERE name='ipResolutionList';" --allow-root --path=$PATH \
| python3 -c "import json,sys; print(list(json.load(sys.stdin).keys()))"
```
### Corrigir (copiar a própria recomendação do Wordfence)
```bash
# Ler antes de escrever
wp db query "SELECT val FROM ${PREFIX}wfconfig WHERE name='detectProxyRecommendation';" --allow-root --path=$PATH
# Aplicar — usa o valor que o próprio Wordfence já recomendou
wp db query "UPDATE ${PREFIX}wfconfig SET val='HTTP_CF_CONNECTING_IP' WHERE name='howGetIPs';" --allow-root --path=$PATH
# Confirmar
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name='howGetIPs';" --allow-root --path=$PATH
```
Sem isto configurado, o rate-limiting de login, os bloqueios de IP do WAF e
o Live Traffic podem estar a agir sobre o IP do proxy Cloudflare em vez do
IP real do atacante — mais grave em sites com muito tráfego atrás do mesmo
edge, onde o "IP" observado pode ser partilhado por múltiplos visitantes.
---
## 4. Interacção com cache de página — `cacheType=disabled` é deliberado
```bash
wp db query "SELECT val FROM ${PREFIX}wfconfig WHERE name='cacheType';" --allow-root --path=$PATH
# → disabled
```
O Wordfence tem o seu próprio sistema de cache de página interno
(`Enable Falcon Engine`), mas em sites do bundle Descomplicar® **a cache de
página já é feita por outro plugin dedicado** (WP Fastest Cache ou
equivalente). `cacheType=disabled` está **correcto** — dois sistemas de
cache HTML activos ao mesmo tempo competem, servem versões desalinhadas e
dificultam o purge coordenado. **Nunca activar o cache Falcon do Wordfence
num site que já tem WP Fastest Cache/WP Meteor a fazer cache de página** —
confirmar sempre qual plugin de cache está activo antes de alterar
`cacheType` num site novo.
---
## 5. Scan settings — cobertura completa
```bash
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name LIKE 'scansEnabled_%';" --allow-root --path=$PATH
```
| Categoria | Chave | Estado (piloto) |
|---|---|---|
| Core WordPress | `scansEnabled_core`, `_coreUnknown`, `_oldVersions` | `1` |
| Plugins/temas | `scansEnabled_plugins`, `_themes` | `1` |
| Malware/ficheiros suspeitos | `scansEnabled_malware`, `_fileContents`, `_fileContentsGSB`, `_suspectedFiles` | `1` |
| Espaço em disco | `scansEnabled_diskSpace` | `1` |
| Utilizadores admin suspeitos | `scansEnabled_suspiciousAdminUsers` | `1` |
| Opções de BD suspeitas | `scansEnabled_options`, `_suspiciousOptions` | `1` |
| Passwords fracas | `scansEnabled_passwds` | `1` |
| Comentários/posts (spam/malware injectado) | `scansEnabled_comments`, `_posts` | `1` |
| GeoIP / Google Safe Browsing | `scansEnabled_geoipSupport`, `_checkGSB` | `1` |
| Config legível externamente | `scansEnabled_checkReadableConfig` | `1` |
| **Detecção de IP mal configurada** | `scansEnabled_checkHowGetIPs` | `1` — ver §3 |
| Estado do WAF | `scansEnabled_wafStatus` | `1` |
| WPScan (directory listing, full path disclosure) | `scansEnabled_wpscan_directoryListingEnabled`, `_wpscan_fullPathDisclosure` | `1` |
| Sensibilidade alta | `scansEnabled_highSense` | `0` (desligado, reduz falsos positivos) |
| Scan binário de imagens | `scansEnabled_scanImages` | `0` (desligado) |
**Avaliação:** cobertura de scan quase total (23 de 25 checks activos),
única omissão relevante é a sensibilidade alta — aceitável para reduzir
ruído em sites com actividade legítima elevada.
---
## 6. Two-Factor / Passkeys / CAPTCHA — tabela SEPARADA `wfls_settings`
**Achado arquitectural importante desta expansão:** o módulo "Login
Security" (2FA, passkeys, CAPTCHA de login, XML-RPC, "remember device",
integração WooCommerce, NTP) **não guarda a sua configuração em
`wfconfig`** — vive numa tabela própria, `wfls_settings`, gerida pela
classe `WordfenceLS\Controller_Settings` (código em
`modules/login-security/classes/controller/settings.php`). Schema:
```
DESCRIBE ${PREFIX}wfls_settings;
name varchar(191) PRI
value longtext -- nota: coluna chama-se "value", não "val"
autoload enum('no','yes')
```
```bash
wp db query "SELECT name, value FROM ${PREFIX}wfls_settings ORDER BY name;" --allow-root --path=$PATH
```
As chaves `loginSec_requireAdminTwoFactor` e `loginSec_enableSeparateTwoFactor`
em `wfconfig` (§2) são a versão **legada** — um switch global único. A
versão actual do plugin é **baseada em papéis (roles)**:
`migrate_admin_2fa_requirements_to_roles()` migra o switch legado para
chaves por role (`required-2fa-role.<role>`) dentro de `wfls_settings` e
depois apaga a chave legada do `wfconfig`. Um site "antigo" pode ainda ter
o valor legado; um site já migrado tem-no ausente/zero e a fonte de
verdade passa a ser `wfls_settings` + capabilities.
### Chaves confirmadas em `wfls_settings` (`emanuelalmeida.pt`)
| Chave | Valor confirmado | O que controla |
|---|---|---|
| `xmlrpc-enabled` | `1` | Exigir 2FA também em pedidos XML-RPC autenticados |
| `allow-xml-rpc` | `1` | Permitir XML-RPC de todo (independente do 2FA) |
| `ip-source` | vazio (`automatic`) | Fonte do IP real para o módulo login-security — **independente** de `howGetIPs` do `wfconfig` (§3); valores: `automatic`, `REMOTE_ADDR`, `X-Forwarded-For`, `X-Real-IP` |
| `ip-trusted-proxies` | vazio | Lista de proxies confiáveis (newline-separated) para o cálculo de IP deste módulo |
| `2fa-user-grace-period` | `10` | Dias de período de graça depois de 2FA se tornar obrigatório por role, antes de bloquear o login |
| `require-2fa.administrator` | vazio | Legado (ver acima); use `required-2fa-role.administrator` |
| `passkey-sign-count-mode` | `reject-lower` | Política anti-clonagem de passkeys: `allow` / `reject-lower` / `reject-lower-and-zero` |
| `passkey-relying-party-override` | vazio | Override do hostname RP para passkeys (multi-domínio) |
| `remember-device` | vazio (`false`) | "Lembrar este dispositivo" para saltar 2FA X dias |
| `remember-device-duration` | `2592000` (30 dias) | Duração do "remember device" em segundos |
| `always-show-login-security-menu` | `1` | Mostrar sempre o menu Login Security mesmo sem nada configurado |
| `enable-auth-captcha` | vazio | CAPTCHA (reCAPTCHA) no formulário de login |
| `recaptcha-site-key` / `recaptcha-secret` | vazio | Credenciais reCAPTCHA — só relevantes se `enable-auth-captcha=1` |
| `recaptcha-threshold` | `0.5` | Limiar de pontuação do reCAPTCHA v3 (0–1) |
| `enable-woocommerce-integration` / `-account-integration` | vazio | Aplicar 2FA/passkeys também na área de conta WooCommerce |
| `use-ntp` | `1` | Sincronizar relógio via NTP para códigos TOTP (evita drift do relógio do servidor) |
| `whitelisted` | vazio | IPs isentos de exigência de 2FA (não confundir com `whitelisted` do `wfconfig`, que é do WAF) |
| `schema-version` | `3` | Versão do schema interno do módulo — não editar manualmente |
### Modelo de permissões por role (capabilities, não settings)
"2FA obrigatório" e "2FA opcional" por role **não** são uma chave de
configuração simples — são geridos como **capabilities** do WordPress,
sincronizadas por `WordfenceLS\Controller_Permissions`:
| Capability | Significado |
|---|---|
| `wf2fa_activate_2fa_self` | O utilizador pode activar/desactivar 2FA na própria conta |
| `wf2fa_activate_2fa_others` | Pode activar/desactivar 2FA de outros utilizadores |
| `wf2fa_manage_settings` | Pode editar as definições do módulo Login Security |
| `wfls_manage_passkey_self` / `_others` | Equivalente para passkeys |
| `wfls_show_login_security` | Capability interna que mostra/esconde o menu (sincronizada automaticamente) |
```bash
wp eval 'var_export(get_role("administrator")->capabilities);' --allow-root --path=$PATH | grep -i 'wf2fa\|wfls'
# Confirmado em emanuelalmeida.pt: administrator TEM wf2fa_activate_2fa_self (pode activar),
# mas isso é OPCIONAL — só se torna obrigatório se required-2fa-role.administrator tiver timestamp > -1
```
O estado "obrigatório desde quando" fica em `required-2fa-role.<role>`
(timestamp Unix) ou `-1` (não obrigatório) — chave dinâmica dentro de
`wfls_settings`, não aparece na lista de defaults porque só existe depois
de alguém a definir pela UI.
### Tabelas de dados do módulo (não config, mas relevantes para auditoria)
| Tabela | Conteúdo |
|---|---|
| `wfls_2fa_secrets` | Segredos TOTP activos por utilizador (contagem = utilizadores com 2FA activo) |
| `wfls_passkeys` | Passkeys (WebAuthn) registadas por utilizador |
| `wfls_role_counts` | Cache de contagens de utilizadores por role, usada pela UI |
---
## 7. Live Traffic — configuração completa
Todas as chaves vivem em `wfconfig` (ao contrário do módulo 2FA). Tabela
de dados: `wflivetraffichuman` (visitas humanas) + `wfhits`, `wfhoover`,
`wfcrawlers`, `wflocs`, `wflogins` (dados de suporte ao Live Traffic e ao
dashboard de tráfego).
| Chave | Valor (piloto) | O que controla |
|---|---|---|
| `liveTrafficEnabled` | `1` | Liga/desliga o registo de Live Traffic |
| `liveTraf_ignorePublishers` | `1` | Não regista visitas de utilizadores autenticados com capacidade de publicar (reduz ruído do próprio staff) |
| `liveTraf_ignoreUsers` | vazio | Lista de usernames (separados por vírgula) a ignorar |
| `liveTraf_ignoreIPs` | vazio | Lista de IPs (separados por vírgula) a ignorar |
| `liveTraf_ignoreUA` | vazio | User-agent a ignorar (substring) |
| `liveTraf_maxRows` | `2000` | Quantidade máxima de registos guardados |
| `liveTraf_maxAge` | `30` | Dias máximos de retenção dos registos |
| `liveTraf_displayExpandedRecords` | `0` | Preferência de UI (registos expandidos por omissão) — não afecta captura |
**Overrides que desligam Live Traffic independentemente da config:**
`wfConfig::liveTrafficEnabled()` força `false` se a constante
`WORDFENCE_DISABLE_LIVE_TRAFFIC` estiver definida, ou se
`WF_IS_WP_ENGINE` for verdadeiro (hosts WP Engine desactivam sempre este
módulo, independentemente do valor em `wfconfig`) — útil para explicar a
um cliente porque "liguei e não aparece nada" nesse tipo de hosting.
```bash
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name LIKE 'liveTraf%' OR name='liveTrafficEnabled';" --allow-root --path=$PATH
```
---
## 8. Blocking completo — País, IP manual, Padrões (UA/Referrer/Hostname)
Todos os bloqueios (manuais e automáticos) vivem numa única tabela,
`wfBlocks7` (não em `wfconfig`), com um campo `type` que distingue a
natureza do bloqueio (`models/block/wfBlock.php`):
| Constante | Valor | Origem |
|---|---|---|
| `TYPE_IP_MANUAL` | 1 | Bloqueio de IP manual pelo admin |
| `TYPE_WFSN_TEMPORARY` | 2 | Resposta automática da Wordfence Security Network (rede partilhada entre sites) |
| `TYPE_COUNTRY` | 3 | Bloqueio por país |
| `TYPE_PATTERN` | 4 | Bloqueio avançado (IP range + hostname + user-agent + referrer) |
| `TYPE_RATE_BLOCK` | 5 | Gerado pelo rate limiting (§9), acção `block` |
| `TYPE_RATE_THROTTLE` | 6 | Gerado pelo rate limiting (§9), acção `throttle` |
| `TYPE_LOCKOUT` | 7 | Lockout de login (falhas de password/2FA) |
| `TYPE_IP_AUTOMATIC_TEMPORARY` / `_PERMANENT` | 8 / 9 | Bloqueios automáticos do WAF, temporário vs promovido a permanente por acção do admin |
```bash
# Ver todos os bloqueios activos por tipo
wp db query "SELECT type, COUNT(*) FROM ${PREFIX}wfblocks7 WHERE expiration=0 OR expiration>UNIX_TIMESTAMP() GROUP BY type;" --allow-root --path=$PATH
```
### Bloqueio por país
Campos guardados em `parameters` (JSON): `blockLogin` (bloquear só a
página de login), `blockSite` (bloquear o resto do site), `countries`
(array de códigos ISO). Comportamento configurado globalmente em
`wfconfig`:
| Chave | Valor (piloto) | O que controla |
|---|---|---|
| `cbl_action` | `block` | O que fazer a um país bloqueado: `block` (503) ou `redirect` (redireccionar) |
| `cbl_redirURL` | vazio | URL de destino se `cbl_action=redirect` |
| `cbl_loggedInBlocked` | `0` | Bloquear país mesmo que o visitante já tenha sessão iniciada |
| `cbl_bypassRedirURL` / `cbl_bypassRedirDest` | vazio | Um visitante de país bloqueado que aceda a este URL relativo é reencaminhado e recebe um cookie de bypass permanente |
| `cbl_bypassViewURL` | vazio | Um visitante permitido que veja este URL relativo recebe o cookie de bypass antecipadamente (para o caso de mudar de país/VPN depois) |
| `cbl_cookieVal` | gerado automaticamente | Valor único do cookie de bypass — regenerar invalida bypasses concedidos |
Nenhum destes está configurado no piloto (site sem bloqueio de país
activo) — confirmar sempre `hasCountryBlock()`/`countryBlocks()` antes de
assumir que não há impacto em visitantes internacionais.
### Bloqueio manual de IP / IP range
```bash
# Confirmar que um IP não está na whitelist antes de o bloquear (a UI faz isto automaticamente)
wp db query "SELECT val FROM ${PREFIX}wfconfig WHERE name='whitelisted';" --allow-root --path=$PATH
```
`wfBlock::isWhitelisted()` verifica dois níveis: a lista manual em
`whitelisted` (`wfconfig`, uma entrada por linha, IP/CIDR) e uma lista de
serviços conhecidos (`whitelistedServices`, JSON — vazio `{}` no piloto;
quando preenchido guarda ranges de IP de serviços como monitorização
externa que o Wordfence nunca bloqueia mesmo com regras activas).
### Bloqueio avançado (Custom Pattern) — hostname / user-agent / referrer
Bloqueio combinável por 4 critérios (pelo menos 1 obrigatório),
`type=custom-pattern` na validação (`wfBlock::validate()`):
| Campo | Formato | Exemplo de uso |
|---|---|---|
| `ipRange` | IP único, CIDR, ou range `1.2.3.4-1.2.3.9` (IPv4 e IPv6 não podem misturar-se no mesmo bloqueio) | Bloquear uma subnet inteira |
| `hostname` | wildcard `*` e `.` (regex `^[a-z0-9\.\*\-]+$`) | Bloquear por reverse-DNS de um datacenter conhecido |
| `userAgent` | substring livre | Bloquear scrapers com UA identificável |
| `referrer` | substring livre | Bloquear tráfego de referrer spam |
```bash
wp db query "SELECT id, blockedTime, reason, expiration FROM ${PREFIX}wfblocks7 WHERE type=4 AND (expiration=0 OR expiration>UNIX_TIMESTAMP());" --allow-root --path=$PATH
```
### Como o Wordfence decide não bloquear crawlers do Google
```bash
wp db query "SELECT val FROM ${PREFIX}wfconfig WHERE name='neverBlockBG';" --allow-root --path=$PATH
# Confirmado no piloto: neverBlockVerified
```
| Valor de `neverBlockBG` | Comportamento |
|---|---|
| `neverBlockVerified` (default, activo no piloto) | Verifica UA **e** faz PTR/reverse-DNS lookup para confirmar que é mesmo um IP do Google antes de isentar de bloqueio |
| `neverBlockUA` | Confia só no User-Agent, sem verificação de IP — menos seguro (fácil de fingir), mas mais barato |
| `treatAsOtherCrawlers` | Não dá tratamento especial a "Google" — pode ser bloqueado como qualquer outro crawler |
---
## 9. Rate Limiting completo — 5 tipos, `throttle` vs `block`
`models/block/wfRateLimit.php` define **5 limitadores independentes**,
cada um com um par de chaves `<nome>` (limiar) + `<nome>_action`
(`throttle` ou `block`). `DISABLED` como valor do limiar desactiva esse
limitador especificamente.
| Limitador | Chave limiar | Chave acção | Valor confirmado (piloto) | Aplica-se a |
|---|---|---|---|---|
| Global | `maxGlobalRequests` | `maxGlobalRequests_action` | `240` / `throttle` | **Qualquer** pedido, humano ou crawler — se activo, sobrepõe-se aos outros 4 |
| Page views de crawlers | `maxRequestsCrawlers` | `maxRequestsCrawlers_action` | `DISABLED` / `throttle` | Só User-Agents identificados como crawler (`wfCrawl::isCrawler`) |
| 404s de crawlers | `max404Crawlers` | `max404Crawlers_action` | `60` / `throttle` | Idem, mas só páginas 404 |
| Page views de humanos | `maxRequestsHumans` | `maxRequestsHumans_action` | `DISABLED` / `throttle` | Visitantes não identificados como crawler |
| 404s de humanos | `max404Humans` | `max404Humans_action` | `60` / `throttle` | Idem, mas só páginas 404 |
```bash
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name LIKE 'max%Requests%' OR name LIKE 'max404%' OR name='blockedTime';" --allow-root --path=$PATH
```
**No piloto, o rate limit global (`maxGlobalRequests=240`) está
ACTIVO** — qualquer IP (humano ou bot) que exceda 240 pedidos na janela
de 1 minuto (tabela `wfTrafficRates`, campo `eMin`) é afectado. Os
limitadores dedicados a crawlers/humanos de page views estão desligados;
só os de 404 (60 num minuto) estão activos, o que é uma configuração
razoável (protege contra scans de 404 sem penalizar navegação normal).
**`throttle` vs `block` não são só rótulos — têm durações e severidade
diferentes:**
- `throttle` → `wfBlock::createRateThrottle()`, duração **fixa de 60
segundos** (`wfRateLimit::rateLimitThrottleDuration()`) — resposta 503
temporária, o visitante volta a conseguir aceder ao fim de 1 minuto.
- `block` → `wfBlock::createRateBlock()`, duração = `blockedTime`
(`wfconfig`, **300s / 5 min no piloto**, configurável) — bloqueio mais
longo, e pode ser promovido a permanente manualmente pelo admin na UI
de Blocking (`TYPE_RATE_BLOCK` → `TYPE_IP_AUTOMATIC_PERMANENT`).
**Excepção comum a 404s permitidos:** `allowed404s` (`wfconfig`, uma
entrada por linha, suporta wildcard `*`) — URLs que nunca contam para os
limitadores de 404, mesmo que devolvam 404 de facto. Default do piloto:
`/favicon.ico`, `/apple-touch-icon*.png`, `/*@2x.png`,
`/browserconfig.xml` (ícones/manifestos pedidos por browsers que geram
404 legítimos e não devem disparar rate limiting).
---
## 10. WAF — arquitectura de armazenamento, modos e categorias de regra
**Achado arquitectural crítico:** para o WAF funcionar *antes* do
WordPress carregar (Extended Protection / auto-prepend), o Wordfence
**não pode depender só da base de dados** — mantém uma cópia da config
relevante em ficheiros PHP no filesystem, fora de `wp_content/plugins`:
```bash
ls -la wp-content/wflogs/
# config.php — config nuclear do WAF (wafStatus, authKey, versão do formato) — 6 chaves só
# config-synced.php — MIRROR de chaves relevantes do wfconfig + bloqueios activos, sincronizado
# sempre que algo muda (país/IP/pattern blocks, howGetIPs, apiKey, etc.)
# config-livewaf.php — config activa/em uso pelo processo WAF autoprepended
# config-transient.php— cache de dados de ataque (grande, ~1.7 MB no piloto)
# rules.php — ficheiro PHP executável (não serializado) com as regras do WAF, incluído
# directamente no bootstrap; NÃO editar/apagar manualmente
# attack-data.php, ips.php — logs internos de ataques/IPs vistos pelo WAF
```
Estes ficheiros começam com `<?php exit('Access denied');
__halt_compiler(); ?>` seguido de dados serializados em PHP — só são
legíveis fazendo `substr()` a partir do offset de `__halt_compiler()` e
`unserialize()`. **Nunca editar estes ficheiros manualmente**; qualquer
alteração de config deve passar pela UI ou por `wfconfig`, que depois é
sincronizada automaticamente para estes ficheiros (via
`wfWAFIPBlocksController::setNeedsSynchronizeConfigSettings()`, chamado
sempre que `wfBlock::create*()` corre).
O motor de armazenamento é escolhido por `WFWAF_STORAGE_ENGINE`
(`waf/bootstrap.php`): **`file`** por omissão (confirmado no piloto —
ficheiros em `wflogs/` existem e têm conteúdo recente), ou **`mysqli`**
forçado automaticamente em hosts WP Engine/Flywheel (ou por constante
explícita), caso em que a config do WAF passa a viver numa ligação MySQL
directa (bypass ao `$wpdb`) em vez de ficheiros.
### Modos e níveis do WAF (`models/firewall/wfFirewall.php`)
| Constante | Valores possíveis | Significado |
|---|---|---|
| `firewallMode()` | `disabled` / `learning-mode` / `enabled` | Estado geral; **`enabled`** no piloto (§1, `waf_status`) |
| `protectionMode()` | `basic` / `extended` | `extended` requer `WFWAF_AUTO_PREPEND` activo (ficheiro `.user.ini`/`.htaccess` a fazer `auto_prepend_file` para `wordfence-waf.php` antes do WordPress arrancar) — sem isto, o WAF só corre depois do `muplugins_loaded`, mais tarde no ciclo, "Basic Protection" |
| `ruleMode()` | `community` / `premium` | Depende de `isPaid` — piloto usa feed `community` (regras com atraso de propagação face a sites Premium) |
| `blacklistMode()` | `disabled` / `enabled` | "Real-Time IP Blocklist" — **só disponível com `isPaid`**, mesmo que `disableWAFBlacklistBlocking=0` |
| `learningModeStatus()` | `false` / `true` / timestamp | Se em Learning Mode, indica se há mudança automática agendada para Enabled (`learningModeGracePeriodEnabled` + `learningModeGracePeriod`) |
### Categorias de regra (fail-score) — confirmado no `rules.php` do piloto
O motor de regras usa 3 categorias de ataque com pontuação de risco
própria (`$this->failScores[...]` em `wflogs/rules.php`):
```bash
ssh server "grep -o \"failScores\\['[a-z]*'\\]\" .../wp-content/wflogs/rules.php | sort -u"
# failScores['rce'] — Remote Code Execution
# failScores['sqli'] — SQL Injection
# failScores['xss'] — Cross-Site Scripting
```
Regras individuais podem ser desactivadas via `disabledRules` — essa
chave vive **dentro do storage do WAF** (`wp-content/wflogs/config.php`
no motor `file`), não em `wfconfig`; o piloto não tem nenhuma regra
desactivada (chave ausente = todas activas). A "pontuação de segurança"
mostrada no Dashboard (`wfFirewall::overallStatus()`) combina: % de
regras activas (35%), blacklist premium activa (35%), Extended Protection
(20%), Rate Limiting/Advanced Blocking ligado (10%) — pesado a favor do
WAF (80% do total), com brute-force protection a contar só 20%.
---
## 11. Diagnostics (Tools → Diagnostics)
Gerado por `lib/wfDiagnostic.php`, sem persistência em BD — é sempre
calculado on-demand quando a página carrega. Secções e o que cada uma
verifica:
| Secção | O que reporta |
|---|---|
| Wordfence Status | Versão instalada, versão da base GeoIP, jobs de cron em atraso (>30min) |
| Filesystem | Se o webserver consegue ler/escrever `wp-content/plugins/wordfence` e `wp-content/wflogs` |
| Wordfence Config | Teste de escrita/leitura real contra `wfconfig` (básico e serializado) — falha aqui = sem permissões de escrita na tabela |
| Wordfence Firewall | Auto-prepend activo, motor de storage configurado vs activo, path dos logs, permissões de ficheiros, conteúdo de `.htaccess`/`.user.ini` relacionado com o WAF |
| MySQL | Versão do MySQL + privilégios reais do utilizador da BD: `SELECT`/`INSERT`/`UPDATE`/`DELETE`/`CREATE`/`ALTER`/`DROP`/`TRUNCATE` (via `SHOW GRANTS FOR current_user()`) |
| PHP Environment | Versão PHP vs mínimo suportado, dono do processo PHP, suporte OpenSSL/cURL (versões, protocolos), `display_errors` |
| Connectivity | Ligação de saída a servidores Wordfence (https), ligação de volta ao próprio site (IPv4 e IPv6 — útil para detectar Cloudflare a bloquear o próprio site) |
| Time | Hora da rede Wordfence, hora do servidor, offset NTP, fonte de tempo usada para TOTP (2FA), fuso horário do WordPress |
**Casos de uso práticos:**
- `userCanAlter`/`userCanDrop` a `false` → migrações/upgrades do plugin
podem falhar silenciosamente; verificar antes de reportar "scan não
actualiza issues".
- `connectToSelf`/`connectToSelfIpv6` a falhar com sinais de Cloudflare
(`cf-mitigated: challenge`, `Cloudflare Ray ID` no body) → o próprio
Cloudflare está a desafiar o loopback request do Wordfence; relevante
para sites do bundle Descomplicar® (todos atrás de Cloudflare via
App for Cloudflare®) em diagnóstico de scans que nunca terminam.
- A secção "WordPress" (via `wfDiagnostic::getWordpressValues()`, não
listada acima por ser genérica ao core) expõe constantes como
`WP_DEBUG`, `DISALLOW_FILE_EDIT`, `WP_CACHE`, `FORCE_SSL_ADMIN` — útil
para confirmar hardening sem abrir `wp-config.php` directamente.
---
## 12. Tools → Import/Export de configuração
**Não é um ficheiro local** (ao contrário de outros plugins do bundle) —
`wfImportExportController` (`lib/wfImportExportController.php`) faz
`export()`/`import()` **através da API do wordfence.com**: o export
gera um token remoto (`export_options`), que depois se cola manualmente
no site de destino (`import_options` + token). Isto exige que ambos os
sites tenham `apiKey` válida e liguem para fora ao wordfence.com — não
funciona offline nem é um download/upload de ficheiro local.
O que é exportado: todas as `wfConfig::getExportableOptionsKeys()`
(essencialmente os grupos `checkboxes` + `otherParams` do
`$defaultConfig`, não o grupo `defaultsOnly`), o agendamento de scans
(`scanSched`, serializado), e todos os registos da tabela de bloqueios
(`wfBlock::exportBlocks()` — inclui bloqueios de país, IP e pattern, mas
não lockouts nem bloqueios automáticos temporários).
**Não é adequado para "clonar config para outro site" via SQL directo**
(copiar a tabela `wfconfig` entre sites não é suportado oficialmente e
arrasta chaves específicas da instalação como `encKey`/`longEncKey` —
preferir sempre o fluxo de token oficial acima ou reconfigurar via UI).
---
## 13. Wordfence Central — gestão remota multi-site
```bash
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name LIKE 'wordfenceCentral%';" --allow-root --path=$PATH
```
**Achado confirmado no piloto: este site ESTÁ ligado ao Wordfence
Central** (`wordfenceCentralConnected=1`), associado a
`it@descomplicar.pt` desde 27/02/2025
(`wordfenceCentralConnectTime=1740679066`), com um `wordfenceCentralSiteID`
próprio (UUID). Central é a consola de gestão remota multi-site do
Wordfence (central.wordfence.com) — permite monitorizar/alertar e (em
planos pagos) configurar Wordfence em vários sites a partir de um único
painel, autenticado via JWT (`wordfenceCentralAccessToken`,
`wordfenceCentralUserSiteAccessToken`) trocado através de um par de
chaves pública/privada gerado no site (`wordfenceCentralPK`,
`wordfenceCentralSecretKey`).
| Chave relevante | O que é |
|---|---|
| `wordfenceCentralConnected` | `1`/`0` — liga/desliga a integração |
| `wordfenceCentralConnectEmail` / `_ConnectTime` | Quem ligou e quando |
| `wordfenceCentralPluginAlertingDisabled` | Se `1`, silencia alertas duplicados no próprio wp-admin porque já chegam via Central |
| `wordfenceCentralConfigurationIssue` | Flag interna — `1` indica falha na renovação do token JWT, útil para diagnosticar "Central parou de reportar" |
| `wordfenceCentralSiteData` | Cache JSON da última resposta da API Central sobre este site |
**Widget do dashboard nativo do WordPress:**
`email_summary_dashboard_widget_enabled` (`wfconfig`, `1` no piloto)
controla o widget "Activity Report" no `/wp-admin/index.php` (WordPress
Dashboard nativo) — **não confundir** com o próprio Dashboard do
Wordfence (`/wp-admin/admin.php?page=Wordfence`, componente Vue sem
chave de config individual para mostrar/esconder secções).
---
## Gotchas / erros comuns
| Sintoma | Causa | Solução |
|---|---|---|
| `wp option get wordfence_...` devolve vazio/erro | Config do Wordfence não vive em `wp_options` | Usar sempre `wp db query` contra `${PREFIX}wfconfig` (colunas `name`/`val`) |
| `SELECT value FROM wfconfig` falha (`Unknown column 'value'`) | A coluna chama-se `val`, não `value` | `SELECT name, val FROM ${PREFIX}wfconfig` |
| Contagem de 2FA por `usermeta.meta_key='wordfence_2fa_secret'` parece desactualizada | Essa é a chave legada; o módulo login-security moderno guarda em tabela própria | Preferir `SELECT COUNT(*) FROM ${PREFIX}wfls_2fa_secrets` |
| `howGetIPs` vazio mas o site parece "funcionar" na mesma | O Wordfence tem fallback de ordem de headers (`HTTP_CF_CONNECTING_IP` → `HTTP_X_REAL_IP` → `REMOTE_ADDR` → `HTTP_X_FORWARDED_FOR`) — mascara o problema em vez de o resolver | Configurar explicitamente `howGetIPs`, não confiar no fallback silencioso |
| `wfpendingissues` vazia mesmo com `scansEnabled_checkHowGetIPs=1` e `howGetIPs` mal configurado | O scan periódico pode não ter corrido recentemente, ou o alerta já expirou/foi dispensado | Não usar `wfpendingissues` vazia como prova de "nada errado" — cruzar sempre com leitura directa da config |
| Activar `loginSec_requireAdminTwoFactor` sem aviso prévio | Bloqueia o fluxo normal de login até o admin configurar um autenticador | Confirmar com o utilizador antes; garantir acesso alternativo (SSH/SQL) para reverter |
| Prefixo de tabela assumido como `wp_` | Sites CWP podem ter prefixo custom (confirmado `wpne_` no piloto) | `wp db prefix --allow-root --path=$PATH` sempre primeiro |
| Editar 2FA/passkeys/CAPTCHA via `UPDATE ${PREFIX}wfconfig` sem efeito | Essas definições vivem em `wfls_settings` (colunas `name`/`value`, não `name`/`val`) — ver §6 | `UPDATE ${PREFIX}wfls_settings SET value='...' WHERE name='...';` |
| Editar regras/bloqueios do WAF directamente em `wp-content/wflogs/*.php` | São ficheiros PHP serializados com `__halt_compiler()`, regenerados a partir da BD — editar à mão desalinha-os da BD e pode partir o WAF | Alterar sempre via `wfconfig`/UI; a sincronização para ficheiro é automática (§10) |
| Achar que `loginSec_requireAdminTwoFactor=0` prova que 2FA nunca é obrigatório | Essa chave é legada; sites migrados usam `required-2fa-role.<role>` dentro de `wfls_settings` | Verificar sempre `required-2fa-role.*` e as capabilities `wf2fa_*` do role antes de concluir (§6) |
| Assumir que `maxRequestsHumans`/`maxRequestsCrawlers`="DISABLED" significa "sem rate limiting nenhum" | Cada um dos 5 limitadores (§9) é independente — `maxGlobalRequests` pode estar activo e a aplicar-se a toda a gente mesmo com os outros 4 desligados | Verificar sempre os 5 pares `<limite>`/`<limite>_action`, não só um |
---
*Fonte: `CONFIG-Plugins-Referencia.md` §8 (Wordfence) + verificação
SQL/SSH ao vivo em `emanuelalmeida.pt` (16-08-2026, WP 7.0.4/PHP 8.2.31,
Wordfence 9.0.0 gratuito) — schema de `wfconfig` (313 chaves confirmadas
via `SELECT DISTINCT name`), contagem real de 2FA/passkeys,
`detectProxyRecommendation`, valores de scan e login security. **Expandido
nesta sessão** com leitura completa do código-fonte do plugin (466
ficheiros PHP, ~147k linhas) — `lib/wfConfig.php` (schema completo de
defaults, 3 grupos: `checkboxes`/`otherParams`/`defaultsOnly`),
`lib/wfJavascriptBridge.php` (labels humanos de todas as opções da UI),
`models/block/wfBlock.php` e `wfRateLimit.php` (tipos de bloqueio e os 5
limitadores de rate limiting), `models/firewall/wfFirewall.php` (modos e
scoring do WAF), `lib/wfDiagnostic.php` (secções de diagnóstico),
`lib/wfImportExportController.php` (mecanismo real de import/export via
token wordfence.com), `lib/wfCentralAPI.php` (integração Central) e o
módulo `modules/login-security/` completo — incluindo a descoberta de que
2FA/passkeys/CAPTCHA vivem numa tabela SQL separada (`wfls_settings`,
colunas `name`/`value`) e que o WAF mantém uma cópia de config em
ficheiros PHP serializados (`wp-content/wflogs/config*.php`), fora de
`wfconfig`. Confirmado ao vivo: `wfls_settings` completa, capabilities
`wf2fa_*`/`wfls_*` do role administrator, ficheiros WAF (`config.php`,
`config-synced.php`, `rules.php`), categorias de regra (`sqli`/`xss`/`rce`)
e ligação activa a Wordfence Central desde 27/02/2025.*
+492
View File
@@ -0,0 +1,492 @@
---
name: wp-activity-log
description: Gestão e auditoria do WP Activity Log (Melapress, slug wp-security-audit-log — rebranding do antigo "WP Security Audit Log") via WP-CLI/SQL em servidores CWP. Cobre consulta ao log de eventos (tabelas wsal_occurrences/wsal_metadata), replicação do padrão stealth mode + log restrito a um único utilizador, confirmação do gotcha "wp-cli bypassa restrições de UI", e gestão de retenção/volume do log. Usar quando "wp activity log", "wsal", "wp security audit log", "log de auditoria wordpress", "quem alterou isto no site", "histórico de acções wordpress", "esconder plugin de auditoria", "stealth mode plugin", "restringir log a um utilizador".
---
# /wp-activity-log — WP Activity Log (Melapress) via WP-CLI/SQL
Plugin de log de auditoria WordPress (equivalente a Syslog para WP). Nome
comercial mudou de "WP Security Audit Log" para **"WP Activity Log"**
(rebranding Melapress) mas o **slug mantém-se `wp-security-audit-log`** — usar
sempre este slug em `wp plugin get/activate/deactivate`, nunca o nome
comercial.
**Fonte:** `CONFIG-Plugins-Referencia.md` §9 + verificação SSH ao vivo nesta
sessão (`emanuelalmeida.pt`, versão `5.6.5`, prefixo de tabelas `wpne_`).
---
## Contexto CWP — sempre obrigatório
```bash
PATH=/home/USER/public_html
PREFIX=$(wp db prefix --allow-root --path=$PATH) # ex.: wpne_, wp_, wpah_
```
**Acesso via SSH:** `server.descomplicar.pt`, porta 9443. **Existe sim um
comando WP-CLI nativo** — corrige o que estava documentado antes: o plugin
regista `WP_CLI::add_command( 'wsal_cli_commands', ... )` em
`classes/class-wp-security-audit-log.php`. Ver §1.1 para a lista completa
de subcomandos. Fora desses 7 comandos, o resto passa por `wp option`,
`wp db query` e `wp eval`.
---
## 1. Identificação e configuração (18 opções `wp_options`)
Prefixos: `wsal_*` (config principal), `Wsal*` (raro), `fs_wsalp` (estado
Freemius). Ler tudo de uma vez:
```bash
wp option list --search='wsal*' --format=table --fields=option_name,option_value --allow-root --path=$PATH
wp option get fs_wsalp --allow-root --path=$PATH
wp plugin get wp-security-audit-log --format=json --allow-root --path=$PATH
```
### Tabela de configuração real (`emanuelalmeida.pt`, verificado ao vivo)
| Opção | Valor | O que faz |
|---|---|---|
| `wsal_restrict-log-viewer` | `only_me` | Só o utilizador em `only-me-user-id` vê o log no wp-admin |
| `wsal_restrict-plugin-settings` | `only_me` | Só esse utilizador altera a configuração do plugin |
| `wsal_only-me-user-id` | `17` | ID do utilizador autorizado — confirmar sempre com `wp user get <ID>` que é a conta de IT correcta, não um utilizador órfão/comprometido |
| `wsal_hide-plugin` | `yes` | **Stealth mode** — esconde o plugin da lista `wp-admin/plugins.php` (filtro `all_plugins`); um atacante com sessão de browser comprometida não sabe que o site está a ser auditado |
| `wsal_mwp-child-stealth-mode` | `yes` | Idem, mas para dashboards multisite externos (ManageWP e similares) |
| `wsal_disabled-alerts` | array de IDs (ver §3) | Tipos de evento que NÃO são registados |
| `wsal_plugin_version` | `5.6.5` | Versão instalada |
| `wsal_freemius_state` / `fs_wsalp` | `skipped` / `no` | Não activou licenciamento Freemius (versão free) |
| `wsal_built-in-notifications` | array serializado | Config de e-mail de resumo diário (`daily_summary_notification`, `daily_email_address`) |
| `wsal_notified_plugin_updates`, `wsal_notified_theme_updates`, `wsal_notified_wp_core_update` | arrays/strings | Cache interna de versões já notificadas — não é config de segurança, ignorar em auditorias |
| `wsal_feature-highlight-notice-show`, `wsal_upgrade-notice-show`, `wsal_notification-modal-dismissed`, `wsal_free-search-try` | flags de UI | Estado de notices dispensados — irrelevante para segurança |
### 1.1 Comandos WP-CLI nativos (`wp wsal_cli_commands <subcomando>`)
Confirmado em `classes/Controllers/WP_CLI/class-wp-cli-commands.php`
(registados desde a v4.6, `set_retention` inclusive). Todos aceitam
`--allow-root --path=$PATH`.
| Subcomando | Argumentos | O que faz |
|---|---|---|
| `remove_wizard` | nenhum | Marca o wizard de setup inicial como dispensado (`setup-modal-dismissed=true`) |
| `remove_daily_notification` | nenhum | Desactiva o resumo diário por e-mail (`built-in-notifications.daily_summary_notification=false`) — dispara evento `6310` |
| `remove_weekly_notification` | nenhum | Desactiva o resumo semanal por e-mail (`weekly_summary_notification=false`) — dispara evento `6319` |
| `disable_enable_alert` | `<alert_id>` | Activa/desactiva um `alert_id` específico (alternativa CLI ao array `wsal_disabled-alerts`) |
| `set_hide_plugin` | `<1\|true\|0\|false>` | Equivalente CLI a `wp option update wsal_hide-plugin` (stealth mode) |
| `login_page_notification` | `--enabled=<bool>` `--text=<html>` | Activa/desactiva o aviso "este site tem log de actividade" na página de login (`wsal_login-page-notification` + `wsal_login-page-notification-text`) — **não confirmado activo em nenhum site auditado**; texto aceita HTML |
| `set_retention` | `--enabled=<bool>` `--pruning-value=<int>` `--pruning-unit=<days\|months\|years>` | Configura a purga automática (pruning) — ver §6 |
```bash
wp wsal_cli_commands disable_enable_alert 1002 --allow-root --path=$PATH
wp wsal_cli_commands set_hide_plugin 1 --allow-root --path=$PATH
wp wsal_cli_commands set_retention --enabled=true --pruning-value=6 --pruning-unit=months --allow-root --path=$PATH
```
---
## 2. Consultar o log de eventos (`wsal_occurrences` + `wsal_metadata`)
O log real vive em **duas tabelas dedicadas** (não em `postmeta`/`options`):
```bash
wp db query "SHOW TABLES LIKE '%wsal%';" --allow-root --path=$PATH
```
### Estrutura de `${PREFIX}wsal_occurrences` (1 linha = 1 evento)
| Campo | Tipo | Nota |
|---|---|---|
| `id` | bigint PK | |
| `site_id` | bigint | Para multisite |
| `alert_id` | bigint | Tipo de evento (ver tabela §3) |
| `created_on` | double | Unix timestamp com microssegundos — usar `FROM_UNIXTIME(created_on)` |
| `client_ip` | varchar | IP de origem |
| `severity` | varchar | Código numérico (ver §3) |
| `object` | varchar | Categoria (`user`, `post`, `plugins`, `system`, ...) |
| `event_type` | varchar | `created`/`modified`/`deleted`/`login`/... |
| `username`, `user_id`, `user_roles` | | Quem fez a acção — `user_id=0` é sistema/anónimo |
| `session_id`, `user_agent` | | |
| `post_status`, `post_type`, `post_id` | | Preenchido só em eventos de conteúdo |
Detalhe extra (nome do plugin alterado, ficheiro, etc.) fica em
`${PREFIX}wsal_metadata` (`occurrence_id`, `name`, `value` — schema
key/value, `LEFT JOIN` por `occurrence_id`).
### Queries úteis
```bash
PREFIX=$(wp db prefix --allow-root --path=$PATH)
# Volume total + janela temporal coberta
wp db query "SELECT COUNT(*) total, FROM_UNIXTIME(MIN(created_on)) primeiro, FROM_UNIXTIME(MAX(created_on)) ultimo FROM ${PREFIX}wsal_occurrences;" --allow-root --path=$PATH
# Últimos 50 eventos de um utilizador específico
wp db query "SELECT FROM_UNIXTIME(created_on) quando, alert_id, object, event_type, client_ip FROM ${PREFIX}wsal_occurrences WHERE username='it' ORDER BY id DESC LIMIT 50;" --allow-root --path=$PATH
# Eventos num intervalo de datas
wp db query "SELECT FROM_UNIXTIME(created_on) quando, username, object, event_type FROM ${PREFIX}wsal_occurrences WHERE created_on BETWEEN UNIX_TIMESTAMP('2026-08-01') AND UNIX_TIMESTAMP('2026-08-16') ORDER BY id;" --allow-root --path=$PATH
# Filtrar por tipo de evento (alert_id) — ex.: falhas de login (1002)
wp db query "SELECT FROM_UNIXTIME(created_on) quando, username, client_ip FROM ${PREFIX}wsal_occurrences WHERE alert_id=1002 ORDER BY id DESC LIMIT 30;" --allow-root --path=$PATH
# Distribuição por tipo de evento (para ver o que domina o log)
wp db query "SELECT alert_id, COUNT(*) total FROM ${PREFIX}wsal_occurrences GROUP BY alert_id ORDER BY total DESC LIMIT 20;" --allow-root --path=$PATH
# Distribuição por severidade
wp db query "SELECT severity, COUNT(*) total FROM ${PREFIX}wsal_occurrences GROUP BY severity ORDER BY total DESC;" --allow-root --path=$PATH
# Ver metadados de um evento específico (ex.: qual plugin foi alterado)
wp db query "SELECT name, value FROM ${PREFIX}wsal_metadata WHERE occurrence_id=<ID>;" --allow-root --path=$PATH
```
---
## 3. Tipos de evento e severidade (dados reais observados, `emanuelalmeida.pt`)
`severity` (código Melapress padrão): `200`=informational, `250`=user
activity, `300`=notification, `400`=warning, `500`=critical.
Top `alert_id` observados em 60.337 eventos (~1,5 anos de histórico) — usar
como referência de "normal" ao auditar um site novo, não como lista
exaustiva (o WSAL tem 100+ tipos de evento definidos):
| `alert_id` | Ocorrências | Interpretação provável |
|---|---|---|
| `6070` | 26.476 | Verificação periódica de sistema (cron interno do WSAL) |
| `1003` | 20.276 | Actividade de sessão/login |
| `2008` | 3.202 | Conteúdo modificado (post/página) |
| `6066` | 2.615 | Verificação periódica de sistema |
| `6069` | 2.329 | Verificação periódica de sistema |
| `1002` | 1.965 | Login falhado |
| `1000` | 389 | Login com sucesso |
| `2053`/`2054` | 305/133 | Conteúdo criado/eliminado |
**Não confiar em interpretações de `alert_id` sem confirmar** contra a
documentação oficial Melapress (`https://melapress.com/support/kb/`) ou o
código do plugin (`wp-content/plugins/wp-security-audit-log/`) — os números
acima são inferência a partir do padrão de volume, não confirmados
individualmente nesta sessão.
### 3.1 Categorias de eventos (mapa completo, `defaults.php`)
Ficheiro central de definições: `defaults.php` (raiz do plugin, função
`set_wsal_alerts()`, ~3.760 linhas). Define **as categorias core** — os
eventos de integrações de terceiros (WooCommerce, Yoast, ACF, Rank Math,
etc.) vivem em ficheiros separados (ver §3.2). Categorias core, por ordem
de aparição no ficheiro (nome + intervalo aproximado de `alert_id`):
| Categoria (grupo topo) | Subcategorias | Intervalo `alert_id` | Conteúdo |
|---|---|---|---|
| Users Logins & Sessions Events | User Activity | `1000`–`1099` | Login/logout, falhas de login, sessões, alteração de password |
| Content & Comments | Content, Categories, Custom Fields, Custom Fields (ACF), Comments, Widgets, Menus, Custom Post Types, Pages | `2000`–`2999` | Posts/páginas criados/editados/eliminados, categorias, comentários, widgets, menus, custom post types, ACF |
| User Accounts | User Profiles, Multisite User Profiles | `4000`–`4999` | Utilizadores criados/editados/eliminados, mudança de role, password reset |
| Plugins & Themes | Plugins, Themes, Themes on Multisite | `5000`–`5599` | Instalação/activação/desactivação/actualização de plugins e temas |
| Activity Logs | Activity log plugin | `6000`–`6099`, `6300`–`6399` | Config do próprio WSAL alterada (ex.: `6310`/`6311`/`6312`/`6319` já usados pelos comandos WP-CLI de notificação — ver §1.1); também cobre eventos de "Notifications & Integrations" (Slack/SMS/e-mail custom) |
| WordPress & System | System | `6000`–`6099` | Cron interno do WSAL, verificações periódicas (os IDs `6066`/`6069`/`6070` de alto volume observados em §3 pertencem aqui) |
| — | WordPress Site Settings | `4700`–`4799` (aprox.) | Alteração de opções core do WP (site title, timezone, permalinks, etc.) |
| — | Email Events | `8800`–`8899` (aprox.) | Falhas de envio de e-mail via `wp_mail` |
| — | Database Events | `9000`–`9099` (aprox.) | Erros de query, updates de schema da BD |
**Nota:** as gamas de `alert_id` acima são aproximadas (extraídas da ordem
de definição em `defaults.php`, não documentadas oficialmente pela
Melapress como ranges fixos) — para confirmar a categoria exacta de um
`alert_id` específico, o método fiável é `grep -A6 "^\s*$ALERT_ID,$"
defaults.php` no código-fonte do servidor.
### 3.2 Alertas de integrações de terceiros (activados só se o plugin correspondente estiver presente)
Ficheiros em `classes/WPSensors/Alerts/` (18 ficheiros, um por integração
suportada) — cada um define o próprio bloco de `alert_id`, só carregado se
o plugin alvo estiver activo no site:
| Ficheiro | Integração | Volume aprox. de alertas |
|---|---|---|
| `class-woocommerce-custom-alerts.php` | WooCommerce | ~417 linhas de definição (produtos, encomendas, cupões, definições da loja) |
| `class-yoast-custom-alerts.php` | Yoast SEO | Meta SEO, sitemaps, redirects |
| `class-rank-math-custom-alerts.php` | Rank Math | Meta SEO, schema, redirects |
| `class-acf-custom-alerts.php` | Advanced Custom Fields | Grupos de campos, campos criados/editados |
| `class-gravity-forms-custom-alerts.php` | Gravity Forms | Formulários, entradas, notificações |
| `class-wpforms-custom-alerts.php` | WPForms | Formulários, entradas |
| `class-wp-2fa-custom-alerts.php` | WP 2FA (Melapress) | Activação/desactivação de 2FA por utilizador |
| `class-memberpress-custom-alerts.php` | MemberPress | Subscrições, membros |
| `class-paid-memberships-pro-custom-alerts.php` | Paid Memberships Pro | Níveis, membros |
| `class-learndash-custom-alerts.php` | LearnDash | Cursos, inscrições |
| `class-bbpress-custom-alerts.php` | bbPress | Fóruns, tópicos |
| `class-tablepress-custom-alerts.php` | TablePress | Tabelas |
| `class-redirection-custom-alerts.php` | Redirection | Regras de redirect |
| `class-termly-custom-alerts.php` | Termly | Consentimento de cookies |
| `class-ai-wp-plugin-alerts.php` | AI (Melapress AI features) | Uso de funcionalidades AI do próprio WSAL |
| `class-multisite-custom-alerts.php` | WordPress Multisite | Criação/eliminação de sites na rede |
| `class-mainwp-server-custom-alerts.php` | MainWP (servidor gerido) | Propagação de definições via dashboard MainWP — ver aviso em `defaults.php:2593` ("definições do WSAL foram sobrepostas a partir do MainWP") |
Confirmar quais estão activos num site: `wp plugin list --status=active
--allow-root --path=$PATH` e cruzar com a tabela acima — só os que
correspondem a um plugin activo geram eventos.
---
## 4. Replicar o padrão de segurança num site novo (checklist)
Baseado na configuração real confirmada em `emanuelalmeida.pt` (avaliada
como "acima da média"):
```bash
PATH=/home/USER/public_html
# 1. Instalar/activar (slug correcto — nome comercial mudou, slug não)
wp plugin install wp-security-audit-log --activate --allow-root --path=$PATH
# 2. Identificar o ID do utilizador de IT/admin de confiança (NÃO um admin genérico partilhado)
wp user list --role=administrator --fields=ID,user_login,user_email --allow-root --path=$PATH
# 3. Restringir visualização do log e config a esse utilizador
wp option update wsal_restrict-log-viewer only_me --allow-root --path=$PATH
wp option update wsal_restrict-plugin-settings only_me --allow-root --path=$PATH
wp option update wsal_only-me-user-id <ID_do_utilizador_IT> --allow-root --path=$PATH
# 4. Activar stealth mode — esconder o plugin da lista wp-admin
wp option update wsal_hide-plugin yes --allow-root --path=$PATH
wp option update wsal_mwp-child-stealth-mode yes --allow-root --path=$PATH
# 5. Confirmar (ler de volta, nunca confiar só no exit code)
wp option get wsal_restrict-log-viewer --allow-root --path=$PATH
wp option get wsal_only-me-user-id --allow-root --path=$PATH
wp user get <ID_do_utilizador_IT> --field=user_login --allow-root --path=$PATH
```
Não mexer em `wsal_disabled-alerts` por defeito — deixar todos os ~900+
tipos de evento activos (contando módulos de terceiros — ver §3.1). O padrão
observado de apenas 13 IDs desligados, todos de baixo valor informativo, é a
postura recomendada; desligar mais do que isso reduz cobertura de auditoria
sem ganho real.
---
## 5. GOTCHA CRÍTICO — stealth mode e "only_me" não protegem contra WP-CLI/SQL
**Confirmado nesta sessão por teste directo:** apesar de `hide-plugin=yes`
esconder o plugin da lista em `wp-admin/plugins.php` (via hook PHP no ecrã
de admin), o plugin continua **totalmente visível e operável via WP-CLI**:
```bash
wp plugin list --allow-root --path=$PATH | grep security-audit
# → wp-security-audit-log,active,none,5.6.5,,on (aparece normalmente)
wp plugin status wp-security-audit-log --allow-root --path=$PATH
# → Status: Active (nenhuma restrição aplicada)
```
**Implicação de segurança real:** o padrão "esconder + restringir a
`only_me`" protege apenas contra um atacante que compromete uma **sessão de
browser wp-admin** de um utilizador administrador genérico (não vê o plugin
na UI, não consegue desactivá-lo nem ler o log pela interface). **Não
protege** contra:
- Acesso SSH/WP-CLI ao servidor (qualquer comando `wp plugin deactivate`,
`wp option update wsal_hide-plugin no`, ou `wp db query "DELETE FROM
wsal_occurrences"` funciona sem qualquer verificação de `only-me-user-id`);
- Acesso directo à base de dados (phpMyAdmin, mysql CLI);
- Um plugin malicioso que corra `update_option()`/`deactivate_plugins()` em
PHP no mesmo processo WordPress (o hook de stealth só filtra a
*renderização* da lista, não a capability real).
Estas restrições são **apenas de UI**, implementadas por filtros como
`all_plugins`/menu — não são um controlo de acesso ao nível dos dados. Ao
avaliar a robustez do log de auditoria de um site, tratar sempre acesso
SSH/WP-CLI e acesso à BD como "full trust" que ignora completamente esta
protecção — a defesa real contra esses vectores é a segurança do próprio
servidor (SSH keys, `mysql` sem exposição externa), não a config do plugin.
---
## 6. Retenção, arquivo e volume do log
O plugin **não faz purga agressiva por defeito** — confirmado:
`emanuelalmeida.pt` tinha 60.337 eventos cobrindo Mar/2025 a Ago/2026 (~1,5
anos) sem gaps aparentes, ocupando `~13 MB` (`wsal_occurrences`) + `~50 MB`
(`wsal_metadata`, guarda os detalhes de cada evento — cresce mais rápido
que a tabela principal).
```bash
# Verificar tamanho actual antes de decidir sobre arquivo/purga
wp db query "SELECT table_name, ROUND(((data_length+index_length)/1024/1024),2) size_mb, table_rows FROM information_schema.TABLES WHERE table_schema=DATABASE() AND table_name LIKE '%wsal%';" --allow-root --path=$PATH
```
### 6.1 Purga automática (pruning) — `wsal_pruning-*`, classe `Plugin_Settings_Helper`/`Settings_Helper`
Ao contrário do que a auditoria pontual anterior sugeria, a purga
automática **não é exclusiva da versão paga** — a lógica e as opções vivem
no core (`classes/Helpers/class-plugin-settings-helper.php` +
`class-settings-helper.php`) e há um comando WP-CLI dedicado (`set_retention`,
ver §1.1). Confirmado nesta expansão: nenhuma destas opções está definida em
`emanuelalmeida.pt` (usa os defaults do código, purga desligada).
| Opção | Default no código | O que faz |
|---|---|---|
| `wsal_pruning-date-e` | `false` (desligado) | Activa/desactiva a purga automática por data |
| `wsal_pruning-date` | calculado por `get_default_pruning_date()` | Data-limite (`strtotime`-compatible, ex. `"6 months"`) — eventos mais antigos são apagados no próximo cron |
| `wsal_pruning-unit` | `months` | Unidade usada no picker da UI (`days`/`months`/`years`) |
| `wsal_pruning-limit-e` | não confirmado | Activa purga por **limite de contagem** (em vez de por data) |
| `wsal_pruning-limit` | `1` (mínimo) | Nº máximo de eventos a manter, quando `pruning-limit-e=true` |
| `wsal_delete-data` | `false` | Se `true`, ao desinstalar o plugin todos os dados (`wsal_occurrences`/`wsal_metadata`/opções) são apagados; se `false`, ficam órfãos na BD |
```bash
# Ler estado actual de purga
wp option list --search='wsal_pruning*' --format=table --allow-root --path=$PATH
# Activar purga a 6 meses via WP-CLI (equivalente ao form da UI)
wp wsal_cli_commands set_retention --enabled=true --pruning-value=6 --pruning-unit=months --allow-root --path=$PATH
wp option get wsal_pruning-date-e --allow-root --path=$PATH
wp option get wsal_pruning-date --allow-root --path=$PATH
```
### 6.2 Arquivo (archiving) para BD externa — `wsal_archiving-*`
Funcionalidade distinta da purga: em vez de apagar, **move** eventos
antigos para uma segunda ligação de base de dados (`Archive_Records::archive()`
em `classes/Entities/Archive/class-archive-records.php`) via cron. Requer
configurar previamente uma "connection" de arquivo (ver §6.3) — sem
connection definida, `is_archiving_set_and_enabled()` devolve `false` e o
cron não corre, mesmo com `archiving-e=yes`.
| Opção | O que faz |
|---|---|
| `wsal_archiving-e` | Liga/desliga o arquivo automático |
| `wsal_archiving-date` + `wsal_archiving-date-type` (`days`/`months`/`years`, default `1`/`months`) | Antiguidade a partir da qual um evento é arquivado |
| `wsal_archiving-run-every` | Frequência do cron de arquivo (default `hourly`) |
| `wsal_archiving-stop` | Pausa temporária do arquivo sem desligar a config |
| `wsal_archiving-cron-started` | Flag interna — cron já agendado |
```bash
wp option list --search='wsal_archiving*' --format=table --allow-root --path=$PATH
```
**Não confirmado activo em nenhum site auditado** (`emanuelalmeida.pt` não
tem nenhuma destas opções definidas).
### 6.3 Espelhamento (mirroring) para BD/serviço externo — `wsal_wsal-mirror-*`
Envia uma cópia em tempo real de cada evento para uma ligação externa
(outra tabela MySQL, ou syslog/SIEM de terceiros via plugin de mirror
dedicado — **o WSAL não tem integração nativa de syslog/SIEM no core**, só
o mecanismo genérico de "connections" reutilizado por mirror e archiving).
`Settings_Helper::get_all_mirrors()` lê todas as opções com prefixo
`WSAL_MIRROR_PREFIX` (`wsal-mirror-`); cada uma é um array serializado
(`connection`, filtros de `alert_id`, etc.).
```bash
wp option list --search='wsal-mirror-*' --format=table --allow-root --path=$PATH
wp option list --search='wsal_connection-*' --format=table --allow-root --path=$PATH
```
Se `database_logging_enabled` for `false` E existir pelo menos um mirror
configurado, o WSAL passa a **não gravar mais em `wsal_occurrences` local**
— todo o log vai só para o(s) destino(s) espelhados. Confirmar sempre
`Settings_Helper::is_database_logging_enabled()` (via `wp eval`) antes de
assumir que as queries SQL do §2 devolvem o histórico completo num site com
mirror activo.
**Nota operacional (aplica-se a §6.1–§6.3):** a ausência de purga é boa
para auditoria (histórico completo disponível), mas em sites com muita
actividade o crescimento é linear e sem limite. Se `wsal_metadata`
ultrapassar dimensões incómodas (centenas de MB), considerar:
- Activar `wsal_pruning-date-e` com uma janela realista (6–12 meses) via
`set_retention`;
- Arquivar eventos antigos para uma tabela `_archive` antes de apagar
(nunca `DELETE` directo sem backup — perde-se rasto de auditoria);
- Configurar uma connection de arquivo (§6.2) se o volume justificar
automação em vez de purga manual.
Não foi necessário agir em `emanuelalmeida.pt` nesta sessão — volume actual
(~63 MB total) é irrelevante para performance.
---
## 7. Notificações, relatórios e integrações
### 7.1 Notificações incorporadas (resumo diário/semanal por e-mail — gratuito)
Opção `wsal_built-in-notifications` (array serializado, constante
`Notifications::BUILT_IN_NOTIFICATIONS_SETTINGS_NAME`). Confirmado em
`emanuelalmeida.pt`: só contém config de resumo diário
(`daily_summary_notification`, `daily_email_address`); a v5.3+ acrescentou
resumo **semanal** (`weekly_summary_notification`, evento `6319` ao
desactivar — ver §1.1 `remove_weekly_notification`).
```bash
wp option get wsal_built-in-notifications --format=json --allow-root --path=$PATH
```
### 7.2 Notificações customizadas (regra "se `alert_id` X ocorrer, notifica Y") — Premium
Opção `wsal_custom-notifications` (constante
`CUSTOM_NOTIFICATIONS_SETTINGS_NAME`) — permite regras condicionais por
utilizador/IP/tipo de evento, com entrega por e-mail, **Slack**
(`classes/Controllers/slack/`) ou **SMS via Twilio**
(`classes/Controllers/twilio/`). Não confirmado activo em nenhum site
auditado; `wsal_notifications` (nota: nome sem `custom-`, é outra opção)
só guarda config global de encurtamento de URLs (`shorten_notification_urls`,
`notification_bitly_shorten_key`) — confirmado vazio/desligado em
`emanuelalmeida.pt`.
```bash
wp option get wsal_custom-notifications --format=json --allow-root --path=$PATH
wp option get wsal_notifications --format=json --allow-root --path=$PATH
```
### 7.3 Exportação e relatórios
- **CSV** — gratuito, core. Acção AJAX `wsal_export_csv_results`
(`classes/Writers/class-csv-writer.php`), disponível no ecrã do log em
wp-admin (botão "Export"). Não há comando WP-CLI dedicado — a exportação
é sempre via browser/AJAX; para extrair dados por CLI usar as queries SQL
do §2 e `wp db query ... --format=csv` como alternativa equivalente.
- **Relatórios agendados** (geração automática + entrega por e-mail,
white-label) — feature **Premium** (`classes/Views/class-premium-features.php`,
`AuditLog.php:470` mostra link de upsell "Get scheduled reports... with
Premium" nos eventos de tipo `2000`/`2001`/`2100`/`4000`). Não existe em
wp-options nem endpoint WP-CLI na versão gratuita.
### 7.4 SIEM/Syslog externo
**Não existe integração nativa de Syslog/SIEM no core do plugin** —
confirmado por ausência de qualquer classe/ficheiro com `syslog`/`SIEM` no
código-fonte (`grep -rl` devolveu zero resultados). A única via de exportar
eventos em tempo real para um sistema externo é o mecanismo de
**espelhamento (mirroring)** descrito em §6.3, que só suporta ligações
configuradas como "connections" do próprio plugin (outra BD MySQL) — não um
protocolo Syslog/CEF nativo. Para integração SIEM real, a Melapress
documenta um add-on comercial separado; confirmar sempre no site do
fornecedor antes de assumir que existe suporte nativo.
---
## 8. Erros comuns
| Sintoma | Causa | Solução |
|---|---|---|
| `wp plugin activate wp-activity-log` falha ("plugin not found") | Usou o nome comercial em vez do slug | Slug é sempre `wp-security-audit-log`, independentemente do rebranding |
| Query em `wsal_occurrences` devolve datas erradas | `created_on` é Unix timestamp `double` (com microssegundos), não `DATETIME` | Usar sempre `FROM_UNIXTIME(created_on)` |
| Achar que o plugin "não está lá" num site com `hide-plugin=yes` | Confundir stealth mode de UI com ausência real | Confirmar sempre via `wp plugin list`/`wp plugin status` — o WP-CLI nunca respeita esta flag (ver §5) |
| `only-me-user-id` aponta para um ID que já não existe/foi desactivado | Utilizador de IT mudou de conta e a opção não foi actualizada | Confirmar sempre com `wp user get <ID>` antes de assumir que a restrição é válida — um ID órfão equivale a log inacessível a todos via UI |
| Achar `wsal_disabled-alerts` vazio = tudo activo | O array serializado inclui sempre um `s:0:""` inicial (artefacto de serialização PHP), não é um alerta desligado real | Ignorar a primeira entrada vazia ao contar IDs desligados |
| Achar que "não há WP-CLI para este plugin" | Confusão histórica — o WSAL regista `wp wsal_cli_commands` desde a v4.6 | Usar `wp cli cmd-dump \| grep wsal` para confirmar; ver §1.1 |
| Achar que purga automática (`pruning`) é só Premium | `set_retention`/`wsal_pruning-*` são core, disponíveis na versão gratuita | Confirmar sempre no código (`class-plugin-settings-helper.php`) antes de recomendar upgrade pago só para ligar retenção |
| Confundir `wsal_notifications` com `wsal_custom-notifications` | Nomes parecidos, funções diferentes — a primeira é só config de encurtamento de URL, a segunda são as regras de alerta customizado (Premium) | Ler sempre as duas e não assumir que uma vazia implica a outra desligada |
| Assumir que o log SQL (§2) tem o histórico completo num site com mirroring activo | Se `database_logging_enabled=false` com mirror configurado, o WSAL deixa de escrever localmente | Confirmar `Settings_Helper::is_database_logging_enabled()` via `wp eval` antes de tirar conclusões de volume/gaps |
---
**Fonte:** `CONFIG-Plugins-Referencia.md` §9 (WP Security Audit Log / WP
Activity Log) + verificação SSH ao vivo nesta sessão (`emanuelalmeida.pt`,
opções `wsal_*`, estrutura e contagem de `wsal_occurrences`/`wsal_metadata`,
teste de bypass `wp plugin list`) + **leitura completa do código-fonte do
plugin nesta expansão** (`wp-content/plugins/wp-security-audit-log/`,
versão `5.6.5`, ~99.000 linhas PHP): `defaults.php` (definições de todos os
eventos core, função `set_wsal_alerts()`), `classes/WPSensors/Alerts/*`
(18 integrações de terceiros), `classes/Controllers/WP_CLI/class-wp-cli-commands.php`
(comandos WP-CLI nativos), `classes/Helpers/class-settings-helper.php` +
`class-plugin-settings-helper.php` (todas as opções `wsal_*` — pruning,
archiving, mirroring, notificações), `classes/Entities/Archive/class-archive-records.php`
(mecanismo de arquivo), `classes/notification/*` (notificações
incorporadas e customizadas, Slack/Twilio), `classes/Views/class-premium-features.php`
e `AuditLog.php` (features gated como Premium: relatórios agendados,
notificações custom, mirroring/archiving).
+4 -1
View File
@@ -276,6 +276,8 @@ done
| `Error establishing database connection` | BD em baixo | `wp db check --allow-root` |
| `Warning: Could not get lock` | Outro processo activo | `wp cli cache clear --allow-root` |
| PHP version wrong | Site usa PHP diferente | `ps aux \| grep "pool USER"` para detectar |
| `wp elementor flush_css` apaga CSS de páginas já OK | O comando limpa **todo** o directório `uploads/elementor/css/`, não só a página-alvo — qualquer página não revisitada fica com `post-N.css` 404/HTML até ser visitada de novo | Depois de qualquer flush, fazer loop `curl` por **todos** os permalinks publicados (`wp post list --field=url`) para forçar regeneração; confirmar `content-type: text/css` em cada `post-N.css`; purgar Cloudflare duas vezes se persistir (origem pode ficar um ciclo atrás do edge) |
| `wp option update NOME --format=json` parte o site (fatal `json_decode(): array given`) | Alguns plugins guardam a config como **string JSON literal** em `wp_options` (o próprio plugin faz `json_decode()` sobre ela). `--format=json` faz o WP-CLI decodificar o input e gravar o **array PHP resultante** (serializado), não a string — o plugin recebe um array onde esperava string e crasha no bootstrap, ANTES de qualquer comando conseguir corrigir (inclui `wp cache flush`, que também precisa de arrancar o WP) | Verificar sempre `wp option get NOME --format=json` primeiro: resultado entre aspas escapadas `"{...}"` = string (editar com `wp db query "UPDATE wp_options SET option_value='...' WHERE option_name='NOME'"`, nunca `--format=json`); resultado sem aspas exteriores = já é array. Se ficar preso (site em baixo, `wp cache flush` também crasha) e o site tiver Redis/Memcached object cache, o valor corrompido fica cacheado e bloqueia até o WP arrancar — limpar directo com `redis-cli -n DB flushdb` (não precisa de WP a correr) antes de repetir a correcção |
---
@@ -328,7 +330,8 @@ wp user list --role=administrator --allow-root --path=$PATH
Registo de erros conhecidos e como evitá-los. Lido automaticamente antes de executar.
```jsonl
{"date":"","issue":"","fix":"","source":"user|auto"}
{"date":"2026-08-16","issue":"wp option update WpFastestCache --format=json gravou array PHP serializado em vez de string JSON; o proprio plugin faz json_decode() sobre o valor e partiu com TypeError fatal no bootstrap do WP, derrubando o site (emanuelalmeida.pt); Redis object cache tinha o valor corrompido cacheado, bloqueando ate wp cache flush (que tambem precisa arrancar o WP)","fix":"nunca usar wp option update --format=json para opcoes que o plugin le como string JSON bruta; editar via wp db query UPDATE wp_options SET option_value=... (sempre string literal, nunca decodificado); se object cache (Redis/Memcached) tiver o valor mau cacheado e qualquer comando wp-cli crashar, limpar directo via redis-cli -n DB flushdb (bypassa o bootstrap do WP por completo) antes de tentar de novo","source":"auto"}
{"date":"2026-08-16","issue":"wp elementor flush_css limpa TODO o directorio uploads/elementor/css/, nao so a pagina visada; um flush pontual para corrigir 1 pagina (post-9.css 404) reintroduziu 404 noutra pagina ja corrigida antes na mesma sessao (post-6.css)","fix":"apos qualquer flush_css, loop curl por TODOS os permalinks publicados (wp post list --field=url) para regenerar tudo; verificar content-type text/css (nao text/html) em cada ficheiro; purgar Cloudflare 2x se persistir 404 no edge apos origem ja estar OK","source":"auto"}
```
*Adicionar nova linha após cada erro corrigido.*
@@ -77,6 +77,9 @@ Verificar em qualquer publisher antes do `wp post create`:
- [ ] ≥1 H2 com a focus_keyword
- [ ] Link externo à fonte (`rel="noopener"`)
- [ ] 1-3 links internos descomplicar.pt no corpo
- [ ] Artigos da categoria **Biblioteca**: terminam sempre com secção FAQ (formato exacto na Secção 10 do PROC — não inventar outro formato, o accordion do site só reconhece formatos já mapeados)
- [ ] `content` nunca vazio — confirmar corpo real, não só os campos RankMath (já aconteceu publicar posts com meta completa e corpo vazio)
- [ ] `post_author` definido pela categoria do conteúdo, não deixado no autor por omissão (mapa categoria→autor na Secção 11 do PROC — Notícias=it, Redes Sociais/Design=juju, Tecnologia=f35, IA=maisum, Gestão/Estratégia/E-commerce=ealmeida, resto da Biblioteca=info)
## Erros RankMath mais comuns
+743
View File
@@ -0,0 +1,743 @@
---
name: wp-fastest-cache
description: Gestão completa do WP Fastest Cache (page cache HTML local) via WP-CLI em servidores CWP. Cobre purga de cache nativa (`wp fastest-cache clear`), diferença entre page cache (WPFC) e object cache (Redis), preload após publicar, minify HTML/CSS/gzip, exclusão de páginas/cookies/user-agents, Clearing Specific Pages, integração CDN (Cloudflare/MaxCDN/genérica) e Varnish, customização do caminho da cache, matriz Free vs Premium, e sequência de purga completa (WPFC + Cloudflare) após deploys ou mudanças de config. Usar quando "wp fastest cache", "wpfc", "purgar cache", "limpar cache wordpress", "cache não actualiza", "preload cache", "minify html", "combine css", "gzip wordpress", "excluir página cache", "cache timeout", "cdn wordpress", "clearing specific pages", "varnish wordpress".
---
# /wp-fastest-cache — Gestão do WP Fastest Cache via WP-CLI
Page cache local (HTML estático em disco) do bundle de performance WP
Descomplicar®. Complementa (não substitui) o Redis Object Cache e o
App for Cloudflare® — os três operam em camadas diferentes, ver §3.
**Fonte:** leitura completa do código-fonte v1.5.0 (versão **Free** — sem
`wp-fastest-cache-premium/` instalado) no servidor `server.descomplicar.pt`,
porta 9443, site piloto `emanuelalmeida.pt`: `wpFastestCache.php` (2746
linhas), `inc/admin.php` (2620 linhas), `inc/cache.php` (1460 linhas),
`inc/preload.php` (894 linhas), `inc/css-utilities.php` (766 linhas),
`inc/cdn.php` (650 linhas), `inc/js-utilities.php` (340 linhas),
`inc/single-preload.php` (346 linhas), `inc/varnish.php` (177 linhas),
`inc/admin-toolbar.php`, `inc/clearing-specific-pages.php`, `inc/column.php`,
`inc/wp-polls.php`, `inc/cli.php`, `uninstall.php`, e todos os templates em
`templates/` (`exclude.php`, `timeout.php`, `preload.php`, `cache_path.php`,
`varnish.php`, `toolbar_settings.php`, `disable_wp_cron.php`,
`clearing_specific_pages.php`, `cdn/cloudflare.php`, `cdn/maxcdn.php`,
`cdn/other.php`, `cdn/file_types.php`, `cdn/specify_sources.php`,
`cdn/exclude_sources.php`) + `CONFIG-Plugins-Referencia.md` §3 +
`BUNDLE-Excelencia-WP.md` §2.2/§2.4 + verificação ao vivo de todas as
`wp_options` prefixadas `WpFastestCache*` via SSH em `emanuelalmeida.pt`,
16-08-2026.
---
## Contexto CWP — sempre obrigatório
```bash
PATH=/home/USER/public_html
wp <comando> --allow-root --path=$PATH
```
**Acesso via SSH:** `ssh server "sudo -u USER /usr/local/bin/wp <comando> --path=$PATH --format=json"`
(alias `server` já configurado; user/path variam por site — ver tabela de
sites de referência no topo desta sessão).
---
## 1. Config real — `wp_options.WpFastestCache` (JSON serializado)
Não existe API `wp option patch` dedicada a este plugin de forma prática —
a option é um objecto JSON simples (não serializado PHP), por isso
**`wp option update` com `--format=json` funciona directamente** (ao
contrário do Rank Math, que usa arrays PHP serializados e exige
`wp option patch`). Internamente, `saveOption()` (`inc/admin.php`) faz
`json_encode($_POST)` do formulário inteiro — ou seja, a option guarda
**exactamente** o subconjunto de checkboxes que estava marcado no momento
de gravar, nunca chaves "off" explícitas (chave ausente = desligada).
```bash
# Ler a config completa
wp option get WpFastestCache --format=json --allow-root --path=$PATH
```
Valor real verificado ao vivo em `emanuelalmeida.pt` (16-08-2026):
```json
{
"wpFastestCacheStatus": "on",
"wpFastestCachePreload": "on",
"wpFastestCachePreload_number": "4",
"wpFastestCacheLoggedInUser": "on",
"wpFastestCacheMobile": "on",
"wpFastestCacheMobileTheme": "on",
"wpFastestCacheNewPost": "on",
"wpFastestCacheMinifyHtml": "on",
"wpFastestCacheMinifyCss": "on",
"wpFastestCacheCombineCss": "on",
"wpFastestCacheGzip": "on",
"wpFastestCacheLBC": "on",
"wpFastestCacheDisableEmojis": "on"
}
```
| Chave | Valor | O que faz |
|---|---|---|
| `wpFastestCacheStatus` | `on` | Liga a cache HTML (interruptor mestre) |
| `wpFastestCachePreload` + `wpFastestCachePreload_number` | `on`, `4` threads | Regenera a cache automaticamente (crawl interno) em vez de esperar pela primeira visita — `4` = threads paralelas do crawler |
| `wpFastestCacheLoggedInUser` | `on` | Serve cache também a utilizadores autenticados (o plugin exclui automaticamente páginas com nonce/admin-bar dinâmicos, não quebra o WP admin) |
| `wpFastestCacheMobile` | `on` | **Free.** Não serve a versão desktop cacheada a dispositivos móveis (cache mobile separada da desktop, ficheiro próprio em `/cache/wpfc-mobile-cache`) |
| `wpFastestCacheMobileTheme` | `on` | **⚠ Só tem efeito com o addon Premium activo** (gate `class_exists("WpFastestCachePowerfulHtml")` em `inc/admin.php:1233`). Neste site a chave está gravada no JSON mas **é ignorada em runtime** porque `wp-fastest-cache-premium/` não está instalado — ver §13 |
| `wpFastestCacheNewPost` | `on` | Limpa a cache automaticamente ao publicar/actualizar conteúdo |
| `wpFastestCacheMinifyHtml` | `on` | Minifica HTML na saída |
| `wpFastestCacheMinifyCss` + `wpFastestCacheCombineCss` | `on` | Minifica e combina ficheiros CSS num único ficheiro |
| `wpFastestCacheGzip` | `on` | Comprime a resposta com gzip |
| `wpFastestCacheLBC` | `on` | Lazy Browser Caching — envia headers `Expires`/`Cache-Control` de longa duração para assets estáticos |
| `wpFastestCacheDisableEmojis` | `on` | Ver §5 |
### 1.1 Todas as chaves possíveis da option `WpFastestCache` (mapeadas do código-fonte)
Lista completa extraída de `grep -roh "wpFastestCache[A-Za-z_]*"` a todo o
código-fonte (`wpFastestCache.php`, `inc/*.php`, `templates/**/*.php`) —
não apenas as 13 activas neste site. Cada chave corresponde a um
`name="..."` de formulário que, quando marcado, é gravado via
`saveOption()`.
**Disponíveis na versão Free (sem gate `class_exists("WpFastestCachePowerfulHtml")`):**
| Chave | Free | O que faz |
|---|---|---|
| `wpFastestCacheStatus` | ✅ | Master switch da cache HTML |
| `wpFastestCachePreload` | ✅ | Preload automático (crawler) — ver §6 |
| `wpFastestCachePreload_number` | ✅ | Nº de threads paralelas do crawler de preload |
| `wpFastestCacheLoggedInUser` | ✅ | Não serve cache a utilizadores autenticados |
| `wpFastestCacheMobile` | ✅ | Cache mobile separada (ficheiros próprios, não é "tema mobile") |
| `wpFastestCacheNewPost` | ✅ | Limpa cache ao publicar |
| `wpFastestCacheNewPost_type` / `_type_all` / `_type_homepage` | ✅ | Âmbito da limpeza ao publicar — só a página nova, ou tudo, ou também a homepage (`templates/newpost.php`) |
| `wpFastestCacheUpdatePost` | ✅ | Limpa cache ao actualizar um post/página existente |
| `wpFastestCacheUpdatePost_type` / `_type_all` / `_type_post` | ✅ | Âmbito da limpeza ao actualizar (`templates/updatepost.php`) |
| `wpFastestCacheMinifyHtml` | ✅ | Minifica HTML |
| `wpFastestCacheMinifyCss` | ✅ | Minifica CSS |
| `wpFastestCacheCombineCss` | ✅ | Combina ficheiros CSS |
| `wpFastestCacheGzip` | ✅ | Gzip |
| `wpFastestCacheLBC` | ✅ | Leverage Browser Caching (headers de expiração) |
| `wpFastestCacheDisableEmojis` | ✅ | Remove script/CSS de emoji do WP core — ver §5 |
| `wpFastestCacheWidgetCache` | ✅ (link de doc é premium, mas o campo em si não está gated no template lido) | Cache de widgets para reduzir queries SQL — **confirmar no wp-admin**, o link de ajuda aponta para a página de marketing Premium mas o checkbox aparece fora do bloco `class_exists` no ficheiro `templates/` correspondente |
| `wpFastestCacheLanguage` | ✅ | Idioma da interface de admin do plugin (select, não afecta o frontend) |
| `wpFastestCachePage` | ✅ (interno) | Campo hidden do formulário que identifica qual sub-página foi submetida (`options`, `deleteCache`, `deleteCssAndJsCache`, `cacheTimeout`) — nunca persiste na option guardada |
| `wpFastestCacheTimeOut` / `_TimeOutHour` / `_TimeOutMinute` | ✅ | Cache Timeout **global legado** (1 única regra, agenda `wp_schedule_event` no hook `wp_fastest_cache`) — não confundir com o Cache Timeout Wizard (§8, `WpFastestCacheExclude`-like, guardado via `wp_fastest_cache_N`) |
**Só têm efeito com `wp-fastest-cache-premium/wpFastestCachePremium.php` activo (gate `class_exists("WpFastestCachePowerfulHtml")` em `inc/admin.php`) — neste site NÃO instalado, ver §13:**
| Chave | O que faria (Premium) |
|---|---|
| `wpFastestCacheMobileTheme` + `_themename` | Cache de um tema mobile dedicado (Mobile Cache) |
| `wpFastestCacheMinifyHtmlPowerFul` | Minificação de HTML mais agressiva ("Minify HTML Plus") |
| `wpFastestCacheMinifyCssPowerFul` | Minificação de CSS mais agressiva ("Minify Css Plus") |
| `wpFastestCacheMinifyJs` | Minifica ficheiros JS |
| `wpFastestCacheCombineJs` | Combina JS no `<head>` |
| `wpFastestCacheCombineJsPowerFul` | Combina JS no `<footer>` ("Combine Js Plus") |
| `wpFastestCacheRenderBlocking` + `_Css` | Elimina render-blocking JavaScript/CSS |
| `wpFastestCacheGoogleFonts` | Carrega Google Fonts de forma assíncrona |
| `wpFastestCacheLazyLoad` + `_type`, `_type_all`, `_type_exceptcontent`, `_keywords`, `_placeholder`, `_exclude_full_size_img` | Lazy load de imagens/iframes com regras avançadas de âmbito e placeholder customizado |
| `wpFastestCacheDelayJS` | Atrasa carregamento de scripts JS até scroll/movimento do rato |
| `wpFastestCachePremium` | Flag interna de licença Premium |
> **Nota crítica:** todo o bloco acima só é renderizado/gravado se
> `class_exists("WpFastestCachePowerfulHtml")` for verdadeiro, o que exige
> o plugin separado `wp-fastest-cache-premium` (licença paga) instalado e
> activo. **Não é apenas "desligado por omissão"** — nos sites do bundle
> Descomplicar (sem addon Premium comprado) estes checkboxes aparecem
> desativados/cinzentos no wp-admin (classe `questionCon disabled`) e os
> respectivos `$_POST` nunca chegam a `saveOption()`. Ver §13 para a
> matriz completa Free vs Premium.
**Chaves de infraestrutura confirmadas como ausentes (não é omissão de leitura):**
| Chave esperada | Estado | Porque fica assim |
|---|---|---|
| `WpFastestCacheVarnish` | ⚪ não configurado | Sem Varnish à frente do site — não aplicável. Ver §10.3 |
| `WpFastestCacheCDN` | ⚪ não configurado | A integração CDN nativa do WPFC não está activa; o purge Cloudflare é feito pelo App for Cloudflare®, não pelo WPFC. Ver §10 |
| `WpFastestCacheExclude` | ⚪ não configurado | Sem regras de exclusão de páginas/cookies/user-agents definidas. Ver §7 |
| `WpFastestCacheCSP` | ⚪ não configurado | Sem regras "Clearing Specific Pages" definidas. Ver §9 |
| `WpFastestCachePathSettings` | ⚪ não configurado | Caminho de cache por omissão (`wp-content/cache`, `wp-content/cache/wpfc-minified`). Ver §11 |
| `WpFastestCacheToolbarSettings` | ⚪ não configurado | Sem restrição de roles na toolbar de admin — ver §12 |
---
## 2. Purgar cache — comando nativo (NÃO é `wp cache flush`)
O plugin regista um comando WP-CLI próprio via `inc/cli.php`
(`WP_CLI::add_command('fastest-cache', 'wpfcCLI')`), carregado
automaticamente quando `WP_CLI` está definido — não precisa de nada extra
no `wp-config.php`. **`wp help fastest-cache` confirma ao vivo (16-08-2026)
que existe um único subcomando: `clear`** — não há `preload`, `status`,
`exclude` nem `cdn` via CLI; essas operações só existem no wp-admin/AJAX.
```bash
# Limpar toda a cache HTML (mantém ficheiros minificados existentes)
wp fastest-cache clear all --allow-root --path=$PATH
# Limpar TUDO, incluindo cache minificada (CSS/HTML combinados) — usar após deploy de CSS/JS
wp fastest-cache clear all and minified --allow-root --path=$PATH
# Limpar só a cache de um ou mais posts/páginas específicos
wp fastest-cache clear --post_id=123 --allow-root --path=$PATH
wp fastest-cache clear --post_id=123,456,789 --allow-root --path=$PATH
```
Código-fonte de `wpfcCLI::clear()` (`inc/cli.php`) — lógica exacta:
1. Se `--post_id=` estiver presente, itera cada ID (split por vírgula) e
chama `singleDeleteCache(false, $post_id)` por post — **não** aceita
`all` em conjunto com `--post_id`;
2. Senão, se `$args[0] == "all"`:
- Se `$args[1] == "and"` e `$args[2] == "minified"` → `deleteCache(true)`;
- Senão → `deleteCache()` (sem o `true`, não limpa minificados);
3. Qualquer outra combinação (`$args[0]` diferente de `all`, ou sintaxe
incompleta de "and minified") → `wrong_usage()`, que imprime um bloco
de erro com link para a doc oficial e sai com `WP_CLI::error_multi_line`.
Internamente, `deleteCache()` — a mesma função que corre quando se clica
"Delete Cache" no wp-admin — faz, por esta ordem:
1. Purga Varnish se `WpFastestCacheVarnish` estiver configurado (não é o
caso nos sites do bundle — ver §10.3);
2. Chama `CdnWPFC::cloudflare_clear_cache()` — **só tem efeito se
`WpFastestCacheCDN` tiver uma entrada `id == "cloudflare"` com
`cdnurl`/`originurl` (email + API token) configurados**. Nos sites do
bundle **não está configurado** — o purge Cloudflare real é feito pelo
**App for Cloudflare®**, não por esta chamada. Não assumir que
`wp fastest-cache clear all` purga o edge Cloudflare;
3. Recria a pasta de preload temporária e relança o crawler de preload
(`set_preload()`);
4. Apaga `wp-content/cache/all`, `wp-content/cache/wpfc-minified` e
`wp-content/cache/wpfc-mobile-cache` (ou os caminhos customizados de
`WpFastestCachePathSettings`, ver §11).
---
## 3. Três camadas de cache — não confundir
| Camada | Plugin/mecanismo | Comando de purga | O que invalida |
|---|---|---|---|
| **Page cache** (HTML em disco) | WP Fastest Cache | `wp fastest-cache clear all and minified` | Ficheiros HTML/CSS/JS gerados em `wp-content/cache/*` |
| **Object cache** (queries à BD) | Redis Object Cache (`redis-cache`, DB 12, Predis) | `wp cache flush` | Chaves no Redis (queries, transients cacheados) |
| **Edge cache** (CDN) | Cloudflare, via App for Cloudflare® | purge no wp-admin do plugin ou API Cloudflare directa | Cache no edge Cloudflare (fora do servidor de origem) |
**Não conflitam entre si** — são camadas independentes e complementares:
o Redis acelera queries PHP mesmo quando o WPFC serve HTML estático (o
WPFC serve o ficheiro sem sequer bootar o WordPress completo em muitos
casos, mas quando bootar — ex. utilizador autenticado — o Redis acelera
essa execução). `wp cache flush` **não limpa a cache HTML do WPFC**, e
`wp fastest-cache clear all` **não limpa o Redis**. Confundir os dois é o
erro mais comum: "limpei a cache e a alteração continua sem aparecer" —
normalmente falta uma das três camadas.
---
## 4. Sequência de cache completa (sempre após alterações de config ou deploy)
Seguir o mesmo padrão do `/rank-math` (`Sequencia de cache`), com o passo
WPFC adicionado:
```bash
wp fastest-cache clear all and minified --allow-root --path=$PATH
wp cache flush --allow-root --path=$PATH
wp transient delete --all --allow-root --path=$PATH
wp rewrite flush --allow-root --path=$PATH
```
Seguido de **purga Cloudflare** (App for Cloudflare®, camada edge — fora
do WP-CLI, via wp-admin ou API Cloudflare com o token da zona). Alterações
que dependem de `robots.txt`, `/llms.txt`, headers ou CSS/JS combinado só
aparecem depois de purgar as três camadas — confirmado no achado real
do `bot_management`/`robots.txt` (`BUNDLE-Excelencia-WP.md` §2.4):
`robots.txt` tinha `cache-control: max-age=315360000` no Cloudflare, e só
mudou depois de purgar Cloudflare + WPFC juntos.
**Regra prática:** depois de qualquer alteração a CSS/JS/tema/Elementor,
purgar sempre **WPFC (`and minified`) + Cloudflare** juntos — nunca só um
dos dois. Alterações a options do WordPress sem impacto em HTML/CSS
(ex. Rank Math meta) só precisam do `wp cache flush` (Redis).
---
## 5. `wpFastestCacheDisableEmojis` — o que faz exactamente
Confirmado em `inc/cache.php:34` — quando esta opção está `on`, o plugin
remove o `<script>` inline `wp-emoji-release.min.js` e o CSS inline de
detecção de emoji que o WordPress core injecta por omissão em todo o
`<head>` (mecanismo nativo para navegadores antigos sem suporte a emoji
Unicode). **Ganho de performance pequeno mas real**: elimina um pedido
JS + um bloco de CSS inline em cada página, sem efeito visual em
navegadores modernos (emoji Unicode nativo funciona sem o script). Sem
riscos conhecidos de quebra — ao contrário de Minify JS/Combine JS
(Premium), não mexe em scripts de plugins/tema.
---
## 6. Preload — configuração completa e rebuild após deploy
`wpFastestCachePreload=on` + `wpFastestCachePreload_number=4` significa
que, após qualquer purga (`deleteCache()`), o plugin relança
automaticamente um crawler interno (`set_preload()`) com 4 threads
paralelas para regenerar a cache das páginas mais visitadas **antes** da
próxima visita real — evita que o primeiro visitante depois de um deploy
apanhe sempre a versão sem cache (mais lenta). Lógica em `inc/preload.php`
(894 linhas) e `inc/single-preload.php` (346 linhas, preload de um único
post após ser publicado/actualizado).
### 6.1 Método de preload (`templates/preload.php`)
O wizard de Preload no wp-admin oferece um `<select>` com dois métodos
(campo interno, não persiste directamente como chave simples — é
serializado dentro da config de preload):
| Método | O que faz |
|---|---|
| `default` | Ordena o conteúdo do mais recente ao mais antigo (`Contents from Newest to Oldest`) — é o método activo se nada for escolhido |
| `sitemap` | Usa um sitemap XML como fonte de URLs a pré-carregar, em vez de percorrer o conteúdo por data |
### 6.2 Tipos de conteúdo incluídos no preload (checkboxes arrastáveis/reordenáveis)
Quando o método é `default`, os seguintes tipos de conteúdo podem ser
marcados individualmente e a ordem entre eles é definida por
drag-and-drop (guardada em `wpFastestCachePreload_order`):
| Checkbox | Tipo de conteúdo |
|---|---|
| `wpFastestCachePreload_homepage` | Homepage |
| `wpFastestCachePreload_post` | Posts |
| `wpFastestCachePreload_category` | Categorias |
| `wpFastestCachePreload_page` | Páginas |
| `wpFastestCachePreload_tag` | Tags |
| `wpFastestCachePreload_attachment` | Anexos (media attachments com página própria) |
| `wpFastestCachePreload_customposttypes` | Custom Post Types registados no site |
| `wpFastestCachePreload_customTaxonomies` | Taxonomias customizadas |
Quando o método é `sitemap`, existe ainda `wpFastestCachePreload_sitemap`
para indicar o URL do sitemap XML a usar como fonte.
### 6.3 Rebuild após deploy
```bash
# Depois de um deploy (código/tema alterado), sequência completa:
wp fastest-cache clear all and minified --allow-root --path=$PATH
wp cache flush --allow-root --path=$PATH
# + purga Cloudflare (App for Cloudflare, fora do WP-CLI)
# O preload arranca sozinho a seguir ao clear — não precisa de comando extra.
# Confirmar que recriou a pasta de cache:
wp eval 'echo is_dir(WP_CONTENT_DIR . "/cache/all") ? "existe" : "vazio, aguardar preload";' --allow-root --path=$PATH
```
---
## 7. Exclusão de páginas, cookies e user-agents — `WpFastestCacheExclude`
Feature separada da option principal, gerida pelo "Exclude Page Wizard"
(`templates/exclude.php`) e persistida em **`wp_options.WpFastestCacheExclude`**
como um array JSON de objectos `{prefix, content, type}` — **não faz parte
do JSON de `WpFastestCache`**. Callback de gravação:
`wpfc_save_exclude_pages_callback()` em `wpFastestCache.php:714`.
```bash
# Ler as regras de exclusão configuradas
wp option get WpFastestCacheExclude --format=json --allow-root --path=$PATH
```
### 7.1 Estrutura de uma regra
| Campo | Valores possíveis | O que faz |
|---|---|---|
| `type` | `page`, `useragent`, `cookie`, `css`, `js` | A que se aplica a regra — página completa (não gera cache), pedido de um User-Agent específico, presença de um cookie, ou exclusão de um ficheiro CSS/JS individual da minificação/combinação |
| `prefix` | `homepage`, `category`, `tag`, `post`, `page`, `archive`, `attachment` (content types); `startwith`, `contain`, `exact`, `regex` (métodos); `googleanalytics`, `yandexclickid`, `woocommerce_items_in_cart` (especiais) | Como o `content` é comparado contra o `REQUEST_URI` (ou o cookie/user-agent) |
| `content` | string livre ou regex | O valor a comparar |
### 7.2 Regras nativas sempre presentes (hardcoded, não editáveis)
Independentemente da configuração, o wizard sempre lista estas regras
como pré-existentes e não removíveis (`editable: false` em
`WpFcExcludePages.insert_existing_rules()`):
| Tipo | Regra | Motivo |
|---|---|---|
| `page`, exact | `wp-login.php` | Nunca cachear a página de login |
| `page`, startwith | `wp-admin` | Nunca cachear o admin |
| `useragent`, contain | `facebookexternalhit` | Bots de preview social não devem receber cache potencialmente desatualizada |
| `useragent`, contain | `LinkedInBot` | idem |
| `useragent`, contain | `WhatsApp` | idem |
| `useragent`, contain | `Twitterbot` | idem |
| `cookie`, contain | `Admin` | Qualquer visitante com cookie de admin (`wordpress_logged_in_...` com role admin) nunca recebe cache |
### 7.3 Efeito no `.htaccess` (Apache) e no PHP (todos os servidores)
Ao gravar (`modify_htaccess_for_exclude()`), o plugin reescreve o bloco
`# Start WPFC Exclude ... # End WPFC Exclude` do `.htaccess` com regras
`mod_rewrite` equivalentes — **isto é a camada rápida (nginx/Apache
servem o miss sem sequer chamar o PHP)**. Em paralelo, `inc/cache.php`
lê `WpFastestCacheExclude` em runtime (`get_option("WpFastestCacheExclude")`,
linha 78) para aplicar as mesmas regras via PHP como segunda camada de
segurança (necessário em nginx, onde não há `.htaccess`).
### 7.4 Bloquear cache de uma página individual via editor
O plugin regista um botão no TinyMCE/Quicktags (`addButtonOnEditor()`,
`checkShortCode()`) que insere `[wpfcNOT]` no conteúdo — qualquer post/página
que contenha este shortcode nunca é cacheada (`$this->blockCache = true`),
sem precisar de criar uma regra de exclusão explícita.
### 7.5 Exclusão automática de wishlist (YITH WooCommerce Wishlist)
Se o plugin `yith-woocommerce-wishlist/init.php` estiver activo,
`exclude_urls()` (`inc/admin.php:163`) adiciona automaticamente uma regra
`exact` para o URL da página de wishlist, sempre que a config é gravada —
comportamento automático, não precisa de intervenção manual.
---
## 8. Cache Timeout — duas mecânicas distintas
### 8.1 Timeout global legado (`wpFastestCacheTimeOut`)
Uma única regra global, gravada como parte da própria option `WpFastestCache`
(`wpFastestCacheTimeOut`, `_TimeOutHour`, `_TimeOutMinute`). Ao gravar
(`addCacheTimeout()`, `inc/admin.php:203`), o plugin:
1. Limpa qualquer agendamento anterior no hook `wp_fastest_cache`
(`wp_clear_scheduled_hook($this->slug())`, onde `slug() == "wp_fastest_cache"`);
2. Agenda um novo `wp_schedule_event()` no mesmo hook, com o schedule
(`hourly`, `daily`, etc.) escolhido em `wpFastestCacheTimeOut` e a hora
exacta calculada a partir de `TimeOutHour`/`TimeOutMinute`.
```bash
# Verificar se há um timeout global agendado
wp cron event list --allow-root --path=$PATH | grep wp_fastest_cache
```
### 8.2 Cache Timeout Wizard — múltiplas regras por página/tipo (`templates/timeout.php`)
Mecanismo mais granular: permite definir **N regras** de purga agendada,
cada uma associada a um subconjunto de páginas (`homepage`, `all`,
`startwith`, `exact`) e a uma frequência (`wp_get_schedules()`, incluindo
schedules customizados que tenham a flag `["wpfc"]` marcada). **Não usa
uma `wp_option` própria** — cada regra vira um evento WP-Cron individual
com hook `wp_fastest_cache_<indice>` (`wpfc_save_timeout_pages_callback()`,
`wpFastestCache.php:832`), e os parâmetros da regra (`prefix`, `content`,
`hour`, `minute`) viajam como argumentos serializados do próprio evento
cron — **inspecionáveis apenas via `wp cron event list` + `wp cron event run`**,
não via `wp option get`.
```bash
# Listar todas as regras de timeout agendadas (uma por evento wp_fastest_cache_N)
wp cron event list --allow-root --path=$PATH | grep 'wp_fastest_cache_'
```
Se o schedule escolhido for `daily`/`onceaday`, a UI mostra também um
selector de Hora/Minuto (fuso do servidor, mostrado ao vivo no wizard via
`current_time("H:i:s")`) para agendar a purga a uma hora fixa em vez de
"daqui a X".
### 8.3 Aviso "Disabled Cron" (`templates/disable_wp_cron.php`)
Se `DISABLE_WP_CRON` estiver definido como `true` no `wp-config.php`, o
botão "+ Add New Timeout" no wp-admin mostra um aviso a explicar que o
WP-Cron foi desligado inteiramente e nenhuma regra de timeout vai
disparar sozinha — só via cron real do sistema a chamar `wp-cron.php`, ou
`wp cron event run --due-now`.
---
## 9. Clearing Specific Pages (CSP) — `WpFastestCacheCSP`
Feature para **purgar páginas adicionais** sempre que a cache é limpa por
qualquer motivo (publicar/actualizar post, clique manual, CLI) —
útil para páginas agregadoras (ex. `/loja/`, `/blog/`) que mostram
conteúdo de múltiplos posts e por isso ficam desatualizadas sem serem
elas próprias editadas. Classe `ClearingSpecificPagesWPFC`
(`inc/clearing-specific-pages.php`), UI em `templates/clearing_specific_pages.php`.
```bash
# Ler as regras CSP configuradas
wp option get WpFastestCacheCSP --allow-root --path=$PATH
```
### 9.1 Estrutura
Array de objectos `{url, order}`, onde:
- `url` **tem de** começar pelo mesmo host que `get_option("home")` —
validado por `check_url()`, senão a gravação falha com
`wp_send_json_error()`;
- Pode conter **no máximo um** wildcard `(.*)`, validado por
`check_wild_card()` — mais do que um wildcard, ou um wildcard mal
colocado (não precedido de `/`), é rejeitado;
- Não pode conter `..` (protecção contra directory traversal,
`preg_match("/\.{2,}/", ...)`);
- `order` é o identificador único da regra (usado para editar/remover).
### 9.2 Exemplo de uso típico
Se `/loja/` lista os últimos produtos e um produto novo é publicado,
adicionar uma regra CSP com `url = https://site.pt/loja/` garante que
`/loja/` é purgado junto com a cache do próprio produto, mesmo que
`wpFastestCacheNewPost_type` esteja configurado apenas para "a página
nova".
---
## 10. Integração CDN e Varnish
### 10.1 CDN genérica (MaxCDN / StackPath / BunnyCDN / CloudFront / "Other") — `WpFastestCacheCDN`
Classe `CdnWPFC` (`inc/cdn.php`, 650 linhas). A option
**`WpFastestCacheCDN`** guarda um **array JSON de integrações** (mais do
que uma CDN pode coexistir), cada entrada identificada por `id`:
`cloudflare`, `maxcdn`, `other` (genérico — cobre StackPath, BunnyCDN,
Amazon CloudFront, KeyCDN, CDN77, que são todos normalizados para `"other"`
internamente em `start_cdn_integration()`/`pause_cdn_integration()`/
`remove_cdn_integration()`).
```bash
wp option get WpFastestCacheCDN --format=json --allow-root --path=$PATH
```
Campos de uma entrada genérica (não-Cloudflare): `id`, `cdnurl` (URL do
CDN), `originurl` (URL de origem, normalmente o próprio domínio),
`status` (ausente = activo, `"pause"` = integração pausada sem remover a
config).
**Âmbito de reescrita de URLs (wizard "Other CDN", `templates/cdn/*`):**
- `templates/cdn/file_types.php` — checkboxes por extensão de ficheiro a
servir via CDN: `aac, avif, css, eot, gif, jpeg, js, jpg, less, mp3,
mp4, ogg, otf, pdf, png, svg, swf, ttf, webm, webp, woff, woff2`
(todos marcados por omissão);
- `templates/cdn/specify_sources.php` — lista de palavras-chave: só URLs
que **contenham** um dos keywords especificados são servidos via CDN
(whitelist opcional, vazio = todos os tipos marcados acima);
- `templates/cdn/exclude_sources.php` — lista de palavras-chave: URLs que
**contenham** um destes keywords **nunca** são servidos via CDN
(blacklist, tem prioridade sobre a whitelist).
Validação em tempo real do URL do CDN (`CdnWPFC::check_url()`) bloqueia
explicitamente: IPv6, IPs decimais/hex/octal, `localhost`, e ranges
privados (`127.`, `10.`, `172.`, `169.`, `100.`, `198.`, `192.`) — com
mensagens de erro específicas conhecidas para CloudFront 403, BunnyCDN
403 e Speedsize 400.
### 10.2 Integração Cloudflare nativa do WPFC
**Distinta do App for Cloudflare®** — esta é a integração embutida no
próprio WP Fastest Cache, activada com `id == "cloudflare"` dentro de
`WpFastestCacheCDN`, campos `cdnurl` (email da conta, ou literal `"wpfc"`
se estiver a usar API Token em vez de Global API Key — ver
`cloudflare_generate_header()`) e `originurl` (a chave/token).
Ao configurar via wizard `templates/cdn/cloudflare.php`, o plugin executa
automaticamente e em sequência (`cloudflare_change_settings()`):
1. **Desactiva o Rocket Loader** da zona Cloudflare (`cloudflare_disable_rocket_loader()`) — evita conflito entre o Rocket Loader (que também reescreve JS) e o Combine/Minify JS do WPFC;
2. Purga a cache Cloudflare (`cloudflare_clear_cache()`);
3. **Define Browser Cache TTL para 6 meses** (`16070400` segundos,
`cloudflare_set_browser_caching()`) — hardcoded, não configurável pela UI;
4. Se o plano Cloudflare for `free`, **remove automaticamente as regras
WebP do `.htaccess`** (`cloudflare_remove_webp()`) — porque o plano
grátis da Cloudflare já faz a sua própria conversão/entrega WebP via
Polish, e as duas camadas juntas causam conflitos;
5. `cloudflare_disable_minify()` está **deprecated** (faz `return` antes
de qualquer chamada à API) porque a Cloudflare descontinuou o Auto
Minify legacy — o código morto continua no ficheiro mas nunca executa.
O Zone ID é resolvido automaticamente por `cloudflare_get_zone_id()`
(paginando `/zones?page=1..2&per_page=1000` da API Cloudflare e
comparando o hostname do site) e fica em cache serializado dentro do
próprio `WpFastestCacheCDN->zone_id` (função `unserialize()`/`serialize()`
directamente sobre um array PHP — não JSON).
```bash
# Confirmar se há integração Cloudflare nativa do WPFC configurada (distinta do App for Cloudflare®)
wp option get WpFastestCacheCDN --format=json --allow-root --path=$PATH | grep -o '"id":"cloudflare"'
```
### 10.3 Varnish (reverse proxy) — `WpFastestCacheVarnish`
Classe `VarnishWPFC` (`inc/varnish.php`). Guarda apenas
`{"server": "<host:porta>"}` (e opcionalmente `"status": "pause"`). Ao
purgar, envia um pedido HTTP `PURGE` (não `GET`/`POST`) para
`<schema>://<server>/.*` com o header `Host` apontado para o domínio real
do site — se o pedido falhar tenta trocar `https://`↔`http://` uma vez
antes de desistir, e trata explicitamente respostas `501` (método PURGE
não permitido pelo Varnish/hosting) com mensagem a sugerir contacto com o
hosting. **Chamado automaticamente dentro de `deleteCache()`** sempre que
`WpFastestCacheVarnish` existir e não estiver em pausa — não é preciso
correr nada manualmente depois de configurado.
```bash
wp option get WpFastestCacheVarnish --allow-root --path=$PATH
```
---
## 11. Customização do caminho da cache — `WpFastestCachePathSettings`
Wizard "Cache Path Customization" (`templates/cache_path.php`) permite
mudar o nome da pasta de cache dentro de `wp-content/` (por omissão
`cache`) e da pasta de assets minificados (por omissão `wpfc-minified`).
Útil quando outro plugin/CDN já usa `wp-content/cache/` para outra coisa,
ou por segurança-por-obscuridade em setups multi-tenant.
```bash
wp option get WpFastestCachePathSettings --allow-root --path=$PATH
```
Estrutura: `{"cachepath": "cache", "optimizedpath": "wpfc-minified"}` —
se a option não existir, o código assume estes dois valores por omissão
(`inc/admin.php:22-29` em `cache_path.php`). **Alterar isto implica que
todos os comandos de purga (`wp fastest-cache clear`, `deleteCache()`)
passam a apagar os novos caminhos** — não há migração automática de
ficheiros já cacheados no caminho antigo, ficam órfãos em disco até
serem apagados manualmente ou por rotação de logs.
---
## 12. Toolbar de admin e acções rápidas por post
### 12.1 Menu na admin bar (`inc/admin-toolbar.php`)
Classe `WpFastestCacheAdminToolbar` adiciona um nó "WP Fastest Cache" na
admin bar do WordPress (frontend e wp-admin), com sub-itens: "Clear Cache
of This Page" (só no frontend), "Clear All Cache", "Clear Cache and
Minified CSS/JS", e (se for multisite) "Clear Cache of All Sites". No
wp-admin, mostra ainda "Toolbar Settings" quando já se está na página de
opções do plugin.
### 12.2 Restringir a toolbar por role (`WpFastestCacheToolbarSettings`)
Wizard `templates/toolbar_settings.php` permite marcar quais **roles não
administrativos** (o `administrator` está sempre incluído e não aparece
na lista) veem o menu "WP Fastest Cache" na admin bar —
`wpfc_toolbar_save_settings_callback()` grava um array `{role: "1"}` em
`wp_options.WpFastestCacheToolbarSettings`.
```bash
wp option get WpFastestCacheToolbarSettings --allow-root --path=$PATH
```
### 12.3 Link "Clear Cache" na listagem de posts/páginas (`inc/column.php`)
Classe `WpFastestCacheColumn` injecta um link "Clear Cache" nas row
actions de `post_row_actions`/`page_row_actions` (a mesma linha onde
aparecem "Edit"/"Trash"/"View" na listagem do wp-admin), com um pedido
AJAX (`wpfc_clear_cache_column`) que chama `singleDeleteCache()` só para
esse post — equivalente a `wp fastest-cache clear --post_id=<ID>` mas
disparado pela UI sem passar pela CLI.
### 12.4 Integração WP-Polls (`inc/wp-polls.php`)
Se o plugin WP-Polls estiver activo, o WPFC intercepta a votação em
enquetes via AJAX próprio (`wpfc_wppolls_ajax_request`) para garantir que
o resultado do voto não fica "preso" numa página cacheada — sem isto, um
visitante veria sempre a enquete por votar mesmo depois de votar, porque
a página HTML estática não reflecte o estado de sessão.
---
## 13. Matriz Free vs Premium (addon `wp-fastest-cache-premium`)
Confirmado no código: `class_exists("WpFastestCachePowerfulHtml")` é o
gate central usado em `inc/admin.php` para decidir o que renderizar/gravar.
Essa classe só existe se o plugin separado
`wp-fastest-cache-premium/wpFastestCachePremium.php` estiver instalado e
activo (licença paga, `get_premium_version()` lê a versão do cabeçalho
desse ficheiro). **Nos sites do bundle Descomplicar este addon NÃO está
instalado** — confirmado por `find` no `wp-content/plugins/` do site
piloto, que só devolve a pasta `wp-fastest-cache/` (Free).
| Funcionalidade | Free | Premium | Onde no código |
|---|---|---|---|
| Page cache HTML, minify HTML/CSS, combine CSS, Gzip, Leverage Browser Caching | ✅ | ✅ | `inc/admin.php` (fora do gate) |
| Preload (crawler automático) | ✅ | ✅ | `inc/preload.php` |
| Exclusão de páginas/cookies/user-agents | ✅ | ✅ | `inc/cache.php`, `WpFastestCacheExclude` |
| Clearing Specific Pages | ✅ | ✅ | `inc/clearing-specific-pages.php` |
| CDN genérica (MaxCDN/Other) + Cloudflare nativa | ✅ | ✅ | `inc/cdn.php` |
| Varnish | ✅ | ✅ | `inc/varnish.php` |
| Cache Timeout (global e por regra) | ✅ | ✅ | `inc/admin.php`, `templates/timeout.php` |
| Disable Emojis | ✅ | ✅ | `inc/cache.php` |
| Mobile cache (separar cache mobile/desktop) | ✅ | ✅ | `wpFastestCacheMobile` |
| **Mobile Theme** (tema mobile dedicado) | ❌ | ✅ | `inc/admin.php:1233`, `wp-fastest-cache-premium/pro/library/mobile-cache.php` |
| **Minify HTML Plus / Minify Css Plus** | ❌ | ✅ | `inc/admin.php:1289,1311` |
| **Minify JS / Combine JS / Combine JS Plus** | ❌ | ✅ | `inc/admin.php:1330-1373` |
| **Render Blocking JS/CSS** | ❌ | ✅ | `inc/admin.php:1400`, doc: `wpfastestcache.com/premium/render-blocking-js/` |
| **Google Fonts optimizado** | ❌ | ✅ | `inc/admin.php:1426` |
| **Lazy Load** (imagens/iframes) | ❌ | ✅ | `inc/admin.php:1450`, `wp-fastest-cache-premium/pro/library/lazy-load.php` (incluído via `cache.php:987`) |
| **Delay JS** | ❌ | ✅ | `inc/admin.php:1488`, `wp-fastest-cache-premium/pro/library/delay-js.php` (incluído via `cache.php:875`) |
| **Image optimisation/compressão** | ❌ | ✅ | `wpFastestCache.php:214`, `WpFastestCacheImageOptimisation` |
| **Logs de actividade** | ❌ | ✅ | `wpFastestCache.php:1556`, `pro/library/logs.php` |
| **DB auto-cleanup** (limpeza de transients/revisões) | ❌ | ✅ | `inc/admin.php:2231`, `pro/templates/db-auto-cleanup.php` |
| **Static file caching avançado** | ❌ | ✅ | `wpFastestCache.php:249`, `pro/library/statics.php` |
**Gotcha de auditoria:** se uma option JSON `WpFastestCache` mostrar
chaves Premium marcadas `"on"` (ex. `wpFastestCacheMobileTheme`,
`wpFastestCacheLazyLoad`) **isso não prova que a funcionalidade está
activa** — só prova que a chave ficou gravada num momento em que o addon
Premium podia estar instalado, ou que veio de uma migração/import. A
única forma fiável de confirmar se o Premium está realmente activo é:
```bash
wp plugin list --path=$PATH --format=json | grep -i "fastest-cache"
# Se só aparecer "wp-fastest-cache" (sem "-premium"), todas as chaves Premium são no-op.
```
---
## 14. Gotchas / erros comuns
| Sintoma | Causa | Solução |
|---|---|---|
| "Limpei a cache e não mudou nada" | Confundir camadas — só correu `wp cache flush` (Redis) ou só `wp fastest-cache clear` (page cache), faltou Cloudflare | Sempre as 3 camadas juntas para mudanças visuais (§4) |
| `wp fastest-cache clear all` não purga Cloudflare | `WpFastestCacheCDN` sem entrada `cloudflare` com credenciais válidas — a chamada `CdnWPFC::cloudflare_clear_cache()` corre mas fica sem efeito por falta de email/API key/zone ID | Purgar Cloudflare à parte (App for Cloudflare® ou API) |
| Comando `wp fastest-cache` "command not found" | `inc/cli.php` só é carregado `if(defined('WP_CLI') && WP_CLI)` — falha se o WP-CLI não inicializar o plugin correctamente (ex. `--skip-plugins`) | Nunca usar `--skip-plugins` com este comando; confirmar `wp plugin is-active wp-fastest-cache` primeiro |
| `wp fastest-cache clear all and minified` dá "Wrong usage" | Sintaxe exacta exigida: exactamente `all`, `and`, `minified` como 3 argumentos posicionais separados por espaço — qualquer typo (`--minified`, `all-minified`) cai em `wrong_usage()` | Copiar o comando literal da doc, sem flags extra |
| Checkbox Premium (Lazy Load, Minify JS, Render Blocking...) não tem efeito nenhum apesar de aparecer marcado no JSON | Addon `wp-fastest-cache-premium` não está instalado — o checkbox nem chega a ser submetido no wp-admin (`disabled`), a chave só sobrevive na option se veio de outro contexto | Confirmar `wp plugin list` (§13) antes de investigar "porque é que a Lazy Load não funciona" |
| Minify JS/Combine JS activados "quebram" o site | Nos sites do bundle **estas opções nem estão disponíveis** (Premium-only) — se aparecerem activas nalgum sítio é sinal de config legada ou de outro plugin | Confirmar se o Premium está mesmo activo antes de desligar; se não estiver, a causa do problema é outra |
| `robots.txt`/`/llms.txt` não reflecte alteração recente | `max-age` alto no Cloudflare + cache HTML do WPFC — as duas mascaram a alteração na origem | Purgar WPFC + Cloudflare juntos (§4), não só um |
| `--post_id=` não limpa nada | ID de post inválido ou página não estava em cache (nunca visitada) | Confirmar que a página tem ficheiro em `wp-content/cache/all/<slug>/` (ou no caminho customizado de `WpFastestCachePathSettings`, §11) antes de assumir falha |
| Regra de exclusão gravada no wp-admin não bloqueia a cache em produção | Site em nginx (sem `.htaccess`) e `WpFastestCacheExclude` não está a ser lido correctamente pelo PHP, ou regex mal escapado (`prefix=regex`) | Testar a regra isoladamente com `wp option get WpFastestCacheExclude --format=json`, validar a regex fora do WordPress primeiro |
| Página agregadora (`/loja/`, `/blog/`) mostra conteúdo desatualizado mesmo com `New Post` activo | `wpFastestCacheNewPost_type` só limpa a página nova, não as páginas que a listam | Adicionar regra Clearing Specific Pages (§9) para essas páginas agregadoras |
| Cache Timeout Wizard "desaparece" depois de gravar | `DISABLE_WP_CRON` está `true` no `wp-config.php` — a regra fica registada como evento mas nunca dispara sozinha | Confirmar cron real do sistema a chamar `wp-cron.php`, ou usar `wp cron event run --due-now` manualmente/via systemd timer |
---
## 15. Decision tree — qual comando/opção usar
| Operação | Comando/Opção |
|---|---|
| Purgar tudo (HTML) | `wp fastest-cache clear all` |
| Purgar tudo incl. minificados (após deploy CSS/JS) | `wp fastest-cache clear all and minified` |
| Purgar só 1+ posts/páginas | `wp fastest-cache clear --post_id=ID[,ID...]` |
| Purgar object cache (Redis) | `wp cache flush` |
| Purgar edge (Cloudflare) | wp-admin App for Cloudflare® ou API Cloudflare directa (ou integração nativa WPFC se configurada, §10.2) |
| Ler config completa | `wp option get WpFastestCache --format=json` |
| Alterar 1 chave da config | `wp option update WpFastestCache '{"...json completo com a chave alterada..."}' --format=json` (é JSON simples, não serializado PHP — reescrever o objecto completo) |
| Confirmar se está a servir cache | `wp eval 'echo is_dir(WP_CONTENT_DIR."/cache/all") ? "sim" : "nao";'` |
| Ver regras de exclusão de páginas/cookies/UA | `wp option get WpFastestCacheExclude --format=json` (§7) |
| Ver regras de purga extra (páginas agregadoras) | `wp option get WpFastestCacheCSP` (§9) |
| Ver integrações CDN configuradas | `wp option get WpFastestCacheCDN --format=json` (§10) |
| Ver config Varnish | `wp option get WpFastestCacheVarnish` (§10.3) |
| Ver caminho de cache customizado | `wp option get WpFastestCachePathSettings` (§11) |
| Ver regras de Cache Timeout (por página) | `wp cron event list | grep wp_fastest_cache_` (§8.2) |
| Ver timeout global legado | `wp cron event list | grep 'wp_fastest_cache '` (hook exacto `wp_fastest_cache`, §8.1) |
| Confirmar se o addon Premium está activo | `wp plugin list --format=json | grep fastest-cache` (§13) |
| Bloquear cache de 1 página sem criar regra | Inserir `[wpfcNOT]` no conteúdo via botão do editor (§7.4) |
---
**Fonte:** `CONFIG-Plugins-Referencia.md` §3 (WP Fastest Cache) +
`BUNDLE-Excelencia-WP.md` §2.2/§2.4 (modelo de performance, achado do
purge Cloudflare+WPFC no `robots.txt`) + leitura directa e completa do
código-fonte do plugin v1.5.0 (Free, sem addon Premium instalado) no
servidor `server.descomplicar.pt`, site piloto `emanuelalmeida.pt`:
`wpFastestCache.php` (2746 linhas, 100% lido), `inc/admin.php` (2620
linhas, secções de settings/save/render lidas integralmente), `inc/cache.php`
(1460 linhas), `inc/preload.php` (894 linhas), `inc/css-utilities.php`
(766 linhas), `inc/cdn.php` (650 linhas, 100% lido), `inc/js-utilities.php`
(340 linhas), `inc/single-preload.php` (346 linhas), `inc/varnish.php`
(177 linhas, 100% lido), `inc/clearing-specific-pages.php` (100% lido),
`inc/admin-toolbar.php`, `inc/column.php`, `inc/wp-polls.php`, `inc/cli.php`
(100% lido), e todos os templates relevantes em `templates/` e
`templates/cdn/` (100% lidos) + `wp help fastest-cache` e `wp option
list --search='WpFastestCache*'` ao vivo em `emanuelalmeida.pt`,
16-08-2026.
+437
View File
@@ -0,0 +1,437 @@
---
name: wp-font-perf
description: Cortar o peso de webfonts e de imagens em background CSS em sites WordPress/Elementor da frota Descomplicar, com medição real de Performance. Cobre o subsetting de fontes de ícones (IconSax, Font Awesome, eicons, elementskit) com `pyftsubset` e rede de segurança por `unicode-range` complementar, a substituição de folhas do Elementor via `style_loader_tag` (e porque um `wp_dequeue_style` não chega), WebP em `background-image` que o WebP Express não cobre, preload de fonte variável para travar CLS, e as duas armadilhas que partem o site ou invalidam medições (purge do Cloudflare avariado, apagar `wpfc-minified`). Usar quando "performance wordpress", "lighthouse", "LCP alto", "CLS", "fontes pesadas", "subset de fontes", "icon fonts", "webfonts", "peso da página", "optimizar site", "core web vitals", "PageSpeed".
---
# /wp-font-perf — Peso de webfonts e imagens CSS, com medição real
Reduzir peso real de páginas WordPress/Elementor na frota Descomplicar. Validado
em piloto em `emanuelalmeida.pt` a 17-08-2026: fontes de ícones **1256KB → 10KB**,
peso total **3125KB → 1258KB**, Performance mobile **60 → 68**, desktop **79 → 88**,
LCP mobile **14,2s → 6,1s**, CLS **0 estável**.
**Fonte:** `04-Stack/02.04-Sistemas/71.Seguranca/BUNDLE-Excelencia-WP.md` §2.2.1
e §2.2.2 (relato completo, incluindo o incidente).
---
## 0. Antes de tudo: medir Performance a sério
**O `lighthouse_audit` do MCP chrome-devtools NÃO mede a categoria Performance.**
Exclui-a por desenho. Um relatório "100/100/100/100" desse MCP pode conviver com
um LCP de 14 segundos — aconteceu, duas sessões seguidas.
```bash
# Instalar/usar Lighthouse local (não depende da quota da API do PSI)
npx -y lighthouse@12 "https://SITE/" --only-categories=performance \
--form-factor=mobile --screenEmulation.mobile --throttling-method=simulate \
--output=json --output-path=/tmp/lh.json --chrome-flags="--headless=new --no-sandbox" --quiet
# desktop
npx -y lighthouse@12 "https://SITE/" --only-categories=performance \
--form-factor=desktop --screenEmulation.disabled --throttling-method=simulate \
--throttling.rttMs=40 --throttling.throughputKbps=10240 --throttling.cpuSlowdownMultiplier=1 \
--output=json --output-path=/tmp/lhd.json --chrome-flags="--headless=new --no-sandbox" --quiet
```
**Regra de evidência: mediana de 5 corridas, nunca uma.** A variância entre
corridas isoladas da mesma configuração chega a **25 pontos** (medido: 69 e 62
para o mesmo estado). Uma corrida não é evidência de nada.
Peso por tipo (é aqui que se decide onde atacar):
```bash
python3 -c "
import json, collections
lr=json.load(open('/tmp/lh.json'))
r=(lr['audits']['network-requests'].get('details') or {}).get('items',[])
by=collections.Counter(); n=collections.Counter()
for x in r:
t=x.get('resourceType') or 'other'; by[t]+=x.get('transferSize',0); n[t]+=1
print('total %.0fKB / %d pedidos' % (sum(by.values())/1024, len(r)))
for t,v in by.most_common(): print(f' {t:12} {n[t]:>3} {v/1024:>7.0f}KB')
print('perf', round(lr['categories']['performance']['score']*100))
for k in ('largest-contentful-paint','cumulative-layout-shift','first-contentful-paint'):
print(' ', k, lr['audits'][k].get('displayValue'))
"
```
---
## 1. Fontes de ícones: o suspeito número um
Sites Elementor com icon packs acumulam megabytes para desenhar dezenas de
glifos. No piloto: **1,2MB para 27 glifos**.
|Fonte|Original|Subset|Glifos usados / existentes|
|---|---|---|---|
|IconSax|355.632 B **TTF**|3.172 B|12 / 890|
|IconSax-Fino|355.692 B **TTF**|536 B|1 / 906|
|eicons|108.000 B|636 B|3 / 515|
|fa-brands-400|81.612 B|1.028 B|4|
|fa-solid-900|78.196 B|780 B|8|
|elementskit (ekiticons)|252KB|**0** (suprimido)|0 / 12|
Icon packs custom carregados pelo Elementor (`uploads/elementor/custom-icons/`)
vêm frequentemente em **TTF/WOFF sem woff2** — formato sem compressão web.
### 1.1 Levantar os glifos em uso em TODO o site
Nunca só na homepage. Varrer todos os permalinks publicados:
```bash
ssh server "sudo -u USER /usr/local/bin/wp post list --post_type=page,post \
--post_status=publish --field=url --path=WEBROOT 2>/dev/null"
```
Depois `curl` a cada URL e recolher as classes (`icon-sax-*`, `fa-*`, `eicon-*`).
Mapear classe → codepoint nos CSS (`content:"\eXXX"`).
**Método mais fiável, no browser** (apanha o que vem de CSS de widgets e não só
de classes no HTML) — itera os pseudo-elementos e lê o `content` computado:
```js
const want = /Font Awesome|eicons|IconSax|elementskit/i;
const hits = {};
for (const el of document.querySelectorAll('*'))
for (const ps of ['::before','::after']) {
const cs = getComputedStyle(el, ps);
if (!want.test(cs.fontFamily || '')) continue;
for (const ch of (cs.content||'').replace(/^["']|["']$/g,'')) {
const cp = ch.codePointAt(0);
if (cp >= 0xE000) (hits[cs.fontFamily.replace(/["']/g,'').split(',')[0].trim()+'|'+cs.fontWeight] ??= new Set()).add(cp.toString(16));
}
}
```
### 1.2 Gerar os subsets
Requer `fonttools` + `brotli` (`pyftsubset` em `~/.local/bin`):
```bash
pyftsubset FONTE.ttf --unicodes=U+E915,U+E969,... --flavor=woff2 \
--layout-features= --no-hinting --desubroutinize --output-file=NOME.subset.woff2
# fonte completa convertida para woff2 (fallback mais leve que o TTF original)
pyftsubset FONTE.ttf --unicodes=U+E000-F8FF --flavor=woff2 \
--layout-features= --no-hinting --output-file=NOME.full.woff2
```
### 1.3 Rede de segurança obrigatória: `unicode-range` complementar
**Nunca servir só o subset.** Declarar duas `@font-face` para a mesma família:
o subset com o `unicode-range` dos glifos em uso, e a fonte completa com o
`unicode-range` **complementar**. Assim um ícone novo adicionado no Elementor
não fica "tofu" — o browser vai buscar a completa só para esse codepoint — e
enquanto isso não acontecer não se paga um byte por ela.
```python
def complement(used, lo=0xE000, hi=0xF8FF):
u=sorted(int(c,16) for c in used); out=[]; s=lo
for c in u:
if c>s: out.append((s,c-1))
s=c+1
if s<=hi: out.append((s,hi))
return out # formatar como U+XXXX ou U+XXXX-YYYY
```
**Confirmar que a completa não é descarregada:**
```js
[...document.fonts].filter(f=>/IconSax|Font Awesome|eicons/.test(f.family))
.map(f=>f.family+'|'+f.weight+'|'+f.status)
// esperado: subsets 'loaded', completas 'unloaded'
```
### 1.4 Substituir a folha, não acrescentar `@font-face` depois
**Um `wp_dequeue_style` não alcança estas folhas** e um `@font-face` acrescentado
na fase de enqueue perde: o Elementor enfileira as folhas de icon library
**durante o render do body** (há widgets com esses ícones no `_elementor_data`),
pelo que `wp_style_is()` devolve `false`, o CSS nosso sai **antes** do original no
documento, perde o desempate "última declaração vence" e **o TTF continua a ser
descarregado** (medido, não presumido).
Padrão correcto:
1. Copiar a folha do Elementor, **manter todas as regras de classe** e trocar só
os `@font-face`. Verificar que não há `url()` fora dos `@font-face` antes de
cortar (backgrounds embutidos).
2. Enfileirar a cópia com `wp_enqueue_style`.
3. Suprimir a original na **impressão**, imune à ordem de enqueue:
```php
add_filter( 'style_loader_tag', function ( $tag, $handle ) {
if ( ! is_admin() && in_array( $handle, array(
'elementor-icons-IconSax', 'elementor-icons-IconSax-Fino',
'elementor-icons-ekiticons', 'elementor-icons-fa-brands',
'elementor-icons-fa-solid', 'elementor-icons',
), true ) ) { return ''; }
return $tag;
}, 10, 2 );
```
Handles reais confirmam-se no HTML: `grep -oE "id='[^']*(icon|ekit)[^']*-css'"`.
### 1.5 Icon packs 100% mortos
`elementor-icons-ekiticons` traz `elementskit.woff` (252KB) e um CSS que o
Lighthouse dá como 100% não usado: define 12 ícones do **painel** do ElementsKit
(tiktok, x-twitter, scroll-reveal, smart-post-list…). Antes de suprimir,
confirmar que as classes presentes no HTML não têm regra:
```bash
ssh server "grep -rlE '(icon-checked|icon-menu-11):before' WEBROOT/wp-content/plugins/ WEBROOT/wp-content/themes/"
# sem resultado = classes órfãs de versão antiga, não desenham glifo
```
---
## 2. CLS: preload da fonte de TEXTO, não das de ícones
Ao reduzir o preload, o heading do hero volta a re-layoutar quando a fonte de
texto chega (no piloto: **CLS 0,213**, com 0,2055 atribuídos a um único `h4`).
**Não preloadar tudo** — foi o erro que criou o problema de LCP. Preloadar **um**
ficheiro: as Google Fonts modernas são **variáveis**, e um só subset latin cobre
os pesos 200-900.
```bash
# descobrir qual ficheiro serve latin + todos os pesos
ssh server "grep -oE '@font-face\{[^}]*\}' WEBROOT/wp-content/uploads/elementor/google-fonts/css/nunito.css" \
# procurar o que tem unicode-range U+0000-00FF (latin) e font-style: normal
```
39KB resolveram o CLS que 1410KB resolviam antes. Confirmar com **5 corridas**,
não uma.
---
## 3. WebP em `background-image`: ponto cego do WebP Express
**O WebP Express reescreve as tags `<img>` do HTML mas não os `background-image`
do CSS.** E em CWP o **nginx serve os estáticos directamente, sem passar pelo
`.htaccess` do plugin**, pelo que também não há negociação por `Accept`.
Resultado no piloto: `foto-1.png` servia **458KB** (36% da página, e era o recurso
LCP) apesar de existir `webp-express/webp-images/uploads/foto-1.png.webp` com
181KB, gerado pelo plugin e nunca servido.
Detectar:
```bash
# candidatos: imagens grandes referenciadas em CSS do Elementor
ssh server "grep -rl 'NOME.png' WEBROOT/wp-content/uploads/elementor/css/"
ssh server "find WEBROOT/wp-content/webp-express -name 'NOME*' -printf '%s %p\n'"
```
Corrigir com `image-set()` (o PNG fica como segundo candidato para browsers sem
suporte). O `!important` é necessário porque o `post-N.css` do Elementor é
enfileirado depois:
```php
$css = sprintf(
'%s{background-image:image-set(url("%s") type("image/webp"),url("%s") type("image/png")) !important}',
$selector, $webp_url, $png_url
);
wp_register_style( 'desc-lcp-webp', false, array(), '1.0.0' );
wp_enqueue_style( 'desc-lcp-webp' );
wp_add_inline_style( 'desc-lcp-webp', $css );
```
Preloadar também a imagem LCP (`background-image` só é descoberta depois de o CSS
ser descarregado e parseado — Load Delay medido de 8,2s = 87% do LCP):
```php
printf('<link rel="preload" as="image" fetchpriority="high" href="%s">', esc_url($url));
```
---
## 4. Outros cortes verificados
- **`dashicons` no frontend:** 750ms de render-blocking + 35KB para **zero**
glifos `dashicons-*` no HTML. Remover só para visitantes não autenticados (a
admin bar precisa dele). `grep -c 'dashicons-'` na página confirma o uso real.
- **Google Fonts remoto duplicado:** `astra-google-fonts` pede o Nunito a
`fonts.googleapis.com` (render-blocking ~770ms + ligação a terceiros) quando o
Elementor já o serve local. Confirmar que o local cobre os pesos antes de
desenfileirar (`grep -c 'font-weight' nunito.css`).
---
## 5. 🔴 Armadilhas que partem o site ou invalidam medições
### 5.1 O purge do Cloudflare reporta sucesso sem purgar
`wp app-for-cf purge-cache` devolve sempre `Success: Cloudflare cache purged.` e
**não purga**. Medido em `emanuelalmeida.pt` (17-08-2026):
```
antes: cf-cache-status: HIT age: 27
comando: Success: Cloudflare cache purged.
depois: cf-cache-status: HIT age: 34 -> 37 -> 40 # nunca invalidou
```
**É bug do comando, não das credenciais.** O token do plugin verifica `active` e
faz `purge_everything` pela API sem problema (`success: true`, `MISS` na visita
seguinte). Com `cfPageCachingSeconds=21600`, as alterações ficam **até 6 horas
invisíveis** — medições no URL limpo medem HTML pré-alteração.
**Purge que funciona** (ferramenta da frota; lê as credenciais do próprio plugin,
resolve a zona real e confirma o efeito, com exit≠0 se continuar `HIT`):
```bash
04-Stack/02.04-Sistemas/71.Seguranca/tools/cf-purge.py \
--account ealmeida --path /home/ealmeida/emanuelalmeida.pt \
--url https://emanuelalmeida.pt/
```
**⚠️ Armadilha ao diagnosticar: há vários plugins Cloudflare com credenciais no
mesmo site.** Registei duas vezes a causa errada ("token inválido", "falta
permissão `Zone → Cache Purge`") por testar as credenciais do plugin errado.
|Opção|Plugin|
|---|---|
|`app_for_cf.cloudflareAuth.token` (`cfut_…`, 53 car.) + `app_for_cf.cfZoneId`|**App for Cloudflare** — as que contam|
|`swcfpc_config.cf_apitoken` / `cf_zoneid`|Super Page Cache — pode ter token inválido e zona errada|
|`wp_turbosafe_cloudflare_api_token`|wp-turbosafe — cifrado, inutilizável|
**Confirmar sempre no código de onde vem a credencial** antes de a declarar
inválida — `app-for-cf` usa `$this->option('cloudflareAuth.token')` em
`src/DigitalPoint/Cloudflare/Api/Cloudflare.php`. E distinguir os erros da API:
`Invalid API Token` (1000) = o valor não corresponde a nenhum token activo;
falta de permissão tem outro código. Se houver `zone_id` divergentes,
`GET /zones?name=<dominio>` desempata.
### 5.2 CLS: o CSS do Elementor sai **depois** de `</head>`
Em sites Elementor, o CSS de cada widget é enfileirado **durante o render do
body** → o browser pinta o header sem estilos e volta a fazer layout quando as
folhas chegam. Em `emanuelalmeida.pt` eram **78KB em 7 folhas** e dava
**CLS 0,45–1,27 em 8 de 10 cargas** (limite bom = 0,1; pico medido 1,319).
**Diagnóstico em dois comandos.** Primeiro, contar folhas fora do head:
```bash
python3 - <<'EOF'
import re,subprocess,time
h=subprocess.run(['curl','-s',f'https://SITE/?x={int(time.time())}'],capture_output=True,text=True).stdout
he=h.find('</head>')
L=[(m.start(),m.group(0)) for m in re.finditer(r"<link[^>]*rel=['\"]stylesheet['\"][^>]*>",h)]
print("HEAD:",len([1 for p,_ in L if p<he]),"| BODY:",len([1 for p,_ in L if p>he]))
for p,l in L:
if p>he: print(" ", (re.search(r"id='([^']+)'",l) or re.search(r'id="([^"]+)"',l)).group(1))
EOF
```
Depois, medir o CLS **a sério** — não confiar no Lighthouse com
`--throttling-method=simulate`, que devolvia 0 quase sempre neste caso. Usar
`PerformanceObserver`, tab nova por corrida, 6 corridas:
```js
await page.evaluateOnNewDocument(()=>{window.__s=[];
new PerformanceObserver(l=>{for(const e of l.getEntries()){
if(!e.hadRecentInput) window.__s.push({v:e.value,t:Math.round(e.startTime),
src:(e.sources||[]).map(s=>s.node&&s.node.tagName+'.'+String(s.node.className||'').split(' ')[0])});
}}).observe({type:'layout-shift',buffered:true});});
await tab.goto(URL,{waitUntil:'networkidle2'});
await new Promise(r=>setTimeout(r,3000));
```
⚠️ **Dois erros de método a evitar** (cometi ambos): registar `evaluateOnNewDocument`
várias vezes na mesma tab **soma observers** e inflaciona o total (vi 5,15 onde o
shift real era 0,79); e um `first-paint` ~100ms antes do shift, com
`domInteractive` no mesmo instante, é a assinatura de CSS no body — não de fontes.
**Correcção:** mu-plugin `elementor-css-head.php` (cópia canónica em
`04-Stack/02.04-Sistemas/71.Seguranca/mu-plugins/`). Aprende que handles saíram
depois do head (`wp_styles()->done` no fecho do `wp_head` vs. no `wp_footer`),
guarda num transient por URL (12h) e antecipa-os para o head na visita seguinte.
Resultado: **CLS 0 em 6/6**, 0 nas medianas Lighthouse mobile/desktop, 0 na
auditoria MCP.
**❌ Não perder tempo com:** desligar `elementor_experiment-e_optimized_css_loading`
(no Elementor 4.x já não desliga o carregamento condicional — as folhas continuam
no body); e descartar o WP Meteor por A/B antes de acusar o defer de JS (`?wp-meteor-nooptimize=true`
deu o mesmo CLS, logo não era ele).
**🔴 Aquecimento obrigatório de DUAS passagens depois do deploy** — no primeiro
pedido de cada URL o mu-plugin ainda não aprendeu nada, e é esse HTML que fica
em cache:
```bash
wp post list --post_type=page,post --post_status=publish --field=url --path=$SITE > /tmp/urls.txt
ssh server "rm -rf $SITE/wp-content/cache/all/*"
while read -r u; do curl -s -o /dev/null "$u?p1=$(date +%s%N)"; done < /tmp/urls.txt # aprende
ssh server "rm -rf $SITE/wp-content/cache/all/*"
while read -r u; do curl -s -o /dev/null "$u?p2=$(date +%s%N)"; done < /tmp/urls.txt # HTML bom
while read -r u; do curl -s -o /dev/null -w "%{http_code} $u\n" "$u"; done < /tmp/urls.txt
```
**Custo aceite:** as folhas passam a bloquear no head em vez do body — mesmos
bytes, mais cedo. Performance mediana mobile 68→64, desktop 88→86 (dentro da
variância). CLS é Core Web Vital e estava 4× a 12× acima do limite: a troca paga-se.
### 5.3 NUNCA apagar `wpfc-minified`
Com `MinifyCss` do WP Fastest Cache ligado, os `<link>` apontam a
`wp-content/cache/wpfc-minified/<hash>/hcd9a.css`. Apagar essa pasta enquanto
existe HTML cacheado (no WPFC **ou no Cloudflare**) a referenciar os hashes
antigos → **CSS a 404 em massa, site sem estilos**. Foi assim que o piloto
apareceu desfigurado ao utilizador.
```bash
# ERRADO
rm -rf WEBROOT/wp-content/cache/wpfc-minified/*
# CERTO — forçar HTML novo sem tocar nos ficheiros minificados
rm -rf WEBROOT/wp-content/cache/all/*
```
### 5.4 Não filtrar a saída do `wp_head` com `ob_start`
Tentar remover uma tag já emitida com `ob_start`/`ob_get_clean` no `wp_head`
partiu o `<head>`. Se a origem de um preload órfão não se encontra, **deixar
documentado em vez de mexer em buffers** — no piloto eram 38KB, não valia.
---
## 6. Verificação de fecho (obrigatória)
1. **Todas as folhas do HTML devolvem conteúdo** — foi isto que apanhou o
incidente sem ambiguidade:
```bash
python3 - <<'EOF'
import subprocess, re
h=subprocess.run(['curl','-s','https://SITE/'],capture_output=True,text=True).stdout
links=re.findall(r"<link rel='stylesheet' id='([^']+)' href='([^']+)'", h)
bad=[]
for hid,href in links:
u = href if href.startswith('http') else 'https:'+href
o=subprocess.run(['curl','-s','-o','/dev/null','-w','%{http_code} %{size_download}',u],capture_output=True,text=True).stdout.split()
if o[0]!='200' or int(o[1])==0: bad.append((hid,o))
print('folhas:',len(links),'| com problema:',len(bad),bad)
EOF
```
2. **Zero glifos em falta, em várias páginas** — medir a largura de cada
codepoint em uso contra a largura de um codepoint ausente ("tofu"):
```js
const c=document.createElement('canvas').getContext('2d');
const w=(fam,wt,cp)=>{c.font=wt+' 32px "'+fam+'"';return c.measureText(String.fromCharCode(parseInt(cp,16))).width;};
const tofu=w('IconSax','400','2764');
// falha se w(fam,wt,cp) === tofu
```
3. **Screenshot** do hero e da secção de ícones.
4. **CLS em 5 corridas**, não uma.
---
## 7. Estado do rollout
Aplicado só em **`emanuelalmeida.pt`** (piloto, 17-08-2026). Pendente #19 do
`BUNDLE-Excelencia-WP.md`: replicar nos outros 6 sites. O padrão do WebP em
background CSS (§3) e o `dashicons` (§4) são os mais prováveis de existirem em
todos.
+518
View File
@@ -0,0 +1,518 @@
---
name: wp-meteor
description: Diagnóstico e configuração do WP Meteor (delay/reorder de JavaScript não crítico) via WP-CLI em servidores CWP. Cobre os 5 módulos reais do plugin — `ultimate-reorder` (atraso de execução de JS até interacção do utilizador, com modo "zero delay" e modo "só interacção"), `exclude` (exclusões manuais por regex), `gdpr` (exclusões automáticas para 8 plugins de cookies), `elementor-animations` e `elementor-pp` (emulação do menu Elementor PowerPack Pro — NÃO é "Elementor Pro") — mais o módulo interno `Compatibility` (sempre activo, não configurável, com exclusões automáticas para dezenas de outros optimizadores). Cobre também os mecanismos de bypass automático (query strings, user agents, page builders, conflito com WP Rocket/Nitropack/FastPixel), o endpoint REST `wpmeteor/v1/detect`, e diagnóstico de CLS/CWV com Chrome DevTools performance trace. Usar quando "wp meteor", "wpmeteor", "atraso javascript", "delay js", "ultimate reorder", "cls alto", "layout shift", "core web vitals wordpress", "facade pattern js", "js não crítico", "elementor powerpack pro", "zero delay mode", "wpmeteordisable", "wpmeteordebug".
---
# /wp-meteor — Diagnóstico e config do WP Meteor (delay de JavaScript)
Plugin de performance de terceiros (`wp-meteor`, autor Aleksandr Guidrevitch,
v`3.4.18`). Melhora TBT/INP **atrasando a execução de JavaScript não crítico**
até à primeira interacção do utilizador ou até um timeout fixo. Config
completa numa única option serializada, `wp_options.wp-meteor-settings`
(array PHP, **5 módulos reais** + versão + campo interno `detected`).
Catálogo confirmado por leitura completa do código-fonte do plugin
(5718 linhas em todos os `.php`, sessão 16-08-2026) — não há módulos
escondidos por descobrir; o que está documentado abaixo é o total.
Confirmado activo em `emanuelalmeida.pt` (16-08-2026, esta sessão). **Não
confirmado noutros sites do bundle** — `wp plugin get wp-meteor` em
`carstuff.pt` devolveu vazio/erro, i.e. não está instalado lá. Verificar
sempre por site antes de assumir que está presente.
---
## Comandos base (SSH + WP-CLI, servidor CWP)
```bash
PATH=/home/USER/public_html # ex: /home/ealmeida/emanuelalmeida.pt
# Ler a option completa (fonte de verdade)
sudo -u USER /usr/local/bin/wp option get wp-meteor-settings --format=json --path=$PATH
# Confirmar versão e estado do plugin
sudo -u USER /usr/local/bin/wp plugin get wp-meteor --format=json --path=$PATH
# Via SSH directo (alias `server` já configurado, porta 9443)
ssh server "sudo -u USER /usr/local/bin/wp option get wp-meteor-settings --path=$PATH --format=json 2>/dev/null"
```
Os avisos PHP (`WP_DEBUG_LOG already defined`) que aparecem em `stderr` são
ruído do ambiente — ignorar, não é erro do plugin.
---
## Estrutura real da option `wp-meteor-settings` (verificado ao vivo)
```json
{
"ultimate-reorder": {
"enabled": true,
"id": "ultimate-reorder",
"delay": "2",
"description": ""
},
"gdpr": {
"enabled": true,
"id": "gdpr",
"description": "Most of the time, you want your GDPR/Cookie to be displayed instantly..."
},
"exclude": {
"enabled": true,
"id": "exclude",
"description": "Specify URLs or keywords or regular expressions... GA and GTM",
"value": []
},
"elementor-animations": { "enabled": true, "id": "elementor-animations", "description": "" },
"elementor-pp": { "enabled": true, "id": "elementor-pp", "description": "" },
"v": "3.4.18"
}
```
| Módulo | Estado real | O que faz |
|---|---|---|
| `ultimate-reorder` | `enabled=true`, **`delay=2`** (segundos, string) | Mecanismo central — ver secção seguinte |
| `gdpr` | `enabled=true` | Impede que o banner de cookies/GDPR seja atrasado — aparece instantaneamente |
| `exclude` | `enabled=true`, **`value: []` — lista vazia** | ⚠️ Lista de URLs/palavras-chave/regex a excluir do atraso. **Nada está excluído hoje.** A própria descrição da opção sugere excluir menus, carrosséis do hero, GA e GTM — nenhum destes está configurado |
| `elementor-animations` | `enabled=true` | Compatibilidade com animações nativas do Elementor (deixa correr entrance animations mesmo com scripts atrasados) |
| `elementor-pp` | `enabled=true` | ⚠️ **Não é "Elementor Pro"** — emula o menu do plugin de terceiros **Elementor PowerPack Pro** (Livemesh). Título real no código: `"Emulate Elementor Powerpack Pro menu"`. Se o site não usa PowerPack Pro, este módulo é irrelevante e pode ficar ligado sem efeito |
| `v` | `"3.4.18"` | Versão do plugin no momento em que a option foi gravada (não confundir com a versão instalada — confirmar sempre via `wp plugin get`) |
| `detected` | não presente hoje (array, quando existe) | Campo interno alimentado pelo endpoint REST `wpmeteor/v1/detect` (JS de admin reporta ferramentas de terceiros detectadas, ex. `"marketo"`, `"hubspot"` — dispara um aviso admin sugerindo contacto com o autor do plugin para optimizações extra) |
Estes **5 módulos são o catálogo completo e definitivo** desta versão do
plugin (`3.4.18`) — confirmado por leitura de todo o código-fonte, não só
da option gravada. **O plugin NÃO tem** (verificado directamente no
código, ausência confirmada, não assumida):
- Lazy loading de imagens/iframes — zero.
- Defer/async de scripts como toggle independente — o mecanismo de delay
é tudo-ou-nada por script, controlado só pelo `ultimate-reorder`.
- Remoção de query strings de assets estáticos.
- Preconnect/dns-prefetch configurável — existe um `preconnect: true`
interno calculado automaticamente (`frontend_adjust_wpmeteor` em
`UltimateReorder.php`), mas não há opção na UI nem chave na option para
o utilizador controlar.
- Critical CSS / CSS não usado.
- Qualquer módulo específico de WooCommerce (existe apenas uma exclusão
automática fixa para um script de troca de classe `woocommerce-no-js` →
`woocommerce-js`, dentro do módulo interno `Compatibility`, não é um
módulo à parte nem é configurável).
Não confundir com outros plugins de performance (WP Rocket, Autoptimize,
etc.) que têm estas features — o WP Meteor é deliberadamente um plugin de
um truque só (delay de JS), bem executado, com módulos de compatibilidade
à volta desse truque.
---
## Como funciona o `ultimate-reorder` (delay/facade pattern)
Padrão clássico de optimização de TBT/INP: em vez de deixar o browser
executar todo o JavaScript da página logo no carregamento (bloqueando a
main thread), o plugin **intercepta os `<script>` da página, remove-os da
execução imediata, e só os dispara quando**:
1. o utilizador interage pela primeira vez (`mousemove`, `scroll`,
`touchstart`, `click`, `keydown` — eventos típicos deste padrão), **ou**
2. o `delay` configurado expira — aqui **2 segundos fixos**.
O que ganha: a página fica "interactive-looking" mais cedo, porque o
JavaScript pesado (analytics, widgets, sliders, tracking) não compete pela
main thread durante o first paint/LCP.
O que pode perder: **qualquer script que controle dimensões, posição ou
visibilidade de elementos visíveis acima da dobra fica também atrasado** —
porque o mecanismo não distingue "JS de tracking" de "JS que a página
precisa para se renderizar correctamente". Se um elemento Elementor depende
de JS para calcular a sua altura final, ícones (`sub-arrow`, Font Awesome),
carrosséis do hero, ou animações que definem `opacity`/`transform` no
carregamento, esses elementos **saltam de posição/tamanho quando o script
finalmente corre** (aos 2s ou na primeira interacção) — a definição exacta
de Cumulative Layout Shift.
### Valores possíveis para `ultimate-reorder.delay` (confirmado no código, `UltimateReorder.php`)
A chave `delay` não é só "número de segundos" — tem 3 comportamentos
distintos consoante o valor gravado (a option guarda string, o PHP faz
`(int)` antes de usar):
| Valor gravado | Comportamento em runtime (`rdelay`, ms enviados ao JS) | Quando usar |
|---|---|---|
| `"0"` | `rdelay = 0` — scripts disparam assim que possível, sem espera fixa (chamado **"zero delay mode"** pelo próprio autor, promovido num aviso admin permanente em `Enqueue.php`) | Sites onde o TBT já é baixo e só se quer o reorder/facade, sem atraso artificial |
| `"2"`, `"5"`, etc. (positivo) | `rdelay = delay * 1000` ms — timeout fixo, script corre ao fim de N segundos **mesmo sem interacção** | Caso geral — garante que scripts de tracking correm mesmo em sessões sem interacção (bounces) |
| `"-1"` (ou qualquer negativo) | `rdelay = 86400000` (24h em ms, na prática "nunca por timeout") — **scripts só correm na primeira interacção real do utilizador**, nunca por timeout | Máxima agressividade de TBT/INP; risco: sessões sem interacção (bots de SEO/Lighthouse, utilizadores que só leem) nunca disparam analytics/pixels |
| módulo `enabled=false` | `rdelay = 0` e o rewrite de scripts não corre — comportamento idêntico a ter o plugin desligado | Teste de isolamento (ver secção de diagnóstico CLS) |
Nota de compatibilidade legada (irrelevante para `v3.4.18` mas presente no
código): em options gravadas por versões `< 2.3.6`, `delay=3` era mapeado
automaticamente para o comportamento "-1" (só interacção). Não se aplica a
options novas.
---
## Configurar exclusões (o gap real encontrado nesta sessão)
`exclude.value` é um array de strings (URL, palavra-chave ou regex) que
identificam o `src`/conteúdo inline de scripts a **não atrasar**. Hoje está
vazio — nada escapa ao atraso de 2s.
```bash
# Adicionar exclusões (substitui o array inteiro — ler primeiro, escrever depois)
sudo -u USER /usr/local/bin/wp option patch update wp-meteor-settings exclude value \
'["gtag","googletagmanager","gtm.js","analytics","elementor-pro/assets/js/frontend","font-awesome"]' \
--format=json --path=$PATH
# Alterar só o delay (ex.: reduzir de 2s para 0.5s para testar impacto)
sudo -u USER /usr/local/bin/wp option patch update wp-meteor-settings ultimate-reorder delay "0.5" --path=$PATH
# Desligar o módulo por completo (teste de isolamento — ver secção seguinte)
sudo -u USER /usr/local/bin/wp option patch update wp-meteor-settings ultimate-reorder enabled false --format=json --path=$PATH
# Confirmar
sudo -u USER /usr/local/bin/wp option get wp-meteor-settings --format=json --path=$PATH
```
**Candidatos reais a excluir** (com base na descrição da própria opção +
nesta investigação de CLS):
- Google Analytics / GTM (`gtag`, `googletagmanager`, `gtm.js`) — nunca
deve ser atrasado, quebra tracking dos primeiros 2s de cada sessão.
- Chat widgets (Tawk, Crisp, WhatsApp floating button) — se aparecem
instantaneamente no design, precisam de correr logo.
- Menus e carrosséis do hero — mencionados explicitamente na descrição da
opção como candidatos.
- Formulários acima da dobra (Elementor Pro Forms) — se o utilizador pode
interagir antes dos 2s, o JS de validação/submit tem de já estar activo.
- JS que controla ícones/dimensões de elementos visíveis no first paint
(Font Awesome, `elementor-pro/assets/js/frontend`) — candidato directo
ligado à investigação de CLS 0,89 desta sessão.
**⚠️ Não alterar em produção sem GATE de mutação** — esta é uma option viva
a servir tráfego real. Qualquer patch aqui é imediato e sem staging.
---
## Exclusões automáticas embutidas (não vêm da option, vêm do código — `GDPR.php` + `Compatibility.php`)
Além do `exclude.value` manual, há **duas camadas de exclusão automática**
que correm sempre que o módulo respectivo está `enabled=true` (ou sempre,
no caso de `Compatibility`), independentes do que está gravado na option.
Isto explica por que alguns scripts "escapam" ao atraso mesmo com
`exclude.value: []`.
### Módulo `gdpr` — regex por plugin de cookies detectado (`blocker/Exclusions/GDPR.php`)
Quando `gdpr.enabled=true` (default), o plugin testa `is_plugin_active()`
para 8 plugins de cookies conhecidos e, para cada um activo, adiciona
automaticamente os padrões correspondentes à lista de "não atrasar":
| Plugin de cookies detectado | Padrões excluídos automaticamente |
|---|---|
| Sempre (independente de plugin) | `ap.legalblink.it/api/scripts/lb_cs.js` (LegalBlink) |
| Complianz (`complianz-gdpr(-premium)`) | id `cmplz-cookiebanner` |
| Cookie Notice (`cookie-notice`) | `var cnArgs`, `/cookie-notice/js/front(.min).js` |
| GDPR Cookie Compliance / Moove (`gdpr-cookie-compliance`) | jQuery, `var moove_frontend_gdpr_scripts`, `var gdpr_consent__strict`, `/gdpr-cookie-compliance/dist/scripts/main.js` |
| Cookie Law Info / CookieYes (`cookie-law-info`) | `var _ckyConfig`, `/cookie-law-info/lite/frontend/js/script(.min).js` |
| EU Cookie Law (`eu-cookie-law-compliance`) | jQuery, `window.hasPolisClConsent` |
| Iubenda (`iubenda-cookie-law-solution`) | `var _iub`, `cdn.iubenda.com` |
| Cookiebot (`cookiebot`) | `consent.cookiebot.com/uc.js` |
| Cookie Script (`cookie-script-com`) | `cookie-script.com/s/…js` |
⚠️ Se `function_exists('is_plugin_active')` falhar (raro, contexto sem
`wp-admin/includes/plugin.php` carregado), o código assume **todos** os
plugins como activos e aplica todos os padrões de uma vez — não é um bug a
corrigir, é o comportamento de fallback do próprio plugin.
### Módulo interno `Compatibility` — sempre activo, sem toggle, sem entrada na option (`blocker/Exclusions/Compatibility.php`)
Este blocker **não aparece em nenhum separador da UI nem tem chave própria
na option** — corre sempre, com prioridade 100 (a mais baixa, ou seja,
aplicado por último). Contém uma lista fixa de ~25 padrões regex para não
atrasar scripts-chave de outros optimizadores/plugins, para evitar
conflitos duplos de optimização:
- Lazy loaders: Autoptimize (`lazyLoadOptions`), LazySizes
(`lazySizesConfig`, `lazysizes(.min).js` — com excepções para Avada/
WPSol addons), WP Rocket lazy load, Rocket Lazy Load CPCSS
(`wprRemoveCPCSS`), Easy Image Optimizer (`eio_lazy_vars`), EWWW
(`ewww_webp_supported`), Smush (`smush-lazy-load(-native).min.js`),
Jetpack Lazy Images (`jetpack-lazy-images-js-enabled`, `lazy-images.js`).
- Fast Velocity Minify (`function fvmuag(`).
- Page builders / temas: Divi (`function et_core_page_resource_fallback(`,
troca de classe `no-js`→`js`), Avada/Fusion (`fusionNavIsCollapsed`),
UpSolution (`window.\$us === undefined`).
- Swift Performance Lazyload (`data-swift-image-lazyload`).
- New Relic (`js-agent.newrelic.com`).
- WooCommerce (troca de classe `woocommerce-no-js`→`woocommerce-js`) — o
único ponto de contacto do plugin com WooCommerce, **não é um módulo
WooCommerce dedicado**.
- WPForms (`var wpforms_settings`).
- Complianz — camada extra dinâmica: lê o filtro `cmplz_known_script_tags`
e a option `complianz_options_custom-scripts` (bloqueios custom
configurados no próprio Complianz) e exclui esses URLs também, em cima
da lista fixa do módulo `gdpr` acima.
---
## Diagnóstico CLS/CWV — ligação à investigação em aberto
Contexto completo: `BUNDLE-Excelencia-WP.md` §2.2 (Performance) e
`CONFIG-Plugins-Referencia.md` §4 (WP Meteor).
Estado da investigação (`emanuelalmeida.pt`, LCP mobile 4,7s / **CLS
desktop 0,89**, não resolvido):
1. **Rocket Loader (Cloudflare, App for Cloudflare®)** — testado e
**descartado** nesta sessão: desligado, purge de cache, novo trace →
CLS manteve-se em 0,89. Não era a causa.
2. **WP Meteor `ultimate-reorder`** — **ainda não testado**, é o candidato
mais forte agora: atraso fixo de 2s, **zero exclusões configuradas**,
mecanismo bem mais agressivo que a reordenação leve do Rocket Loader.
O `<i class="fa">` (Font Awesome) encontrado no HTML durante a
investigação do CLS vem exactamente deste módulo.
Este é o próximo passo concreto documentado — **não uma conclusão**. Testar
antes de decidir se se desliga o módulo, se se reduz o `delay`, ou se se
preenche a lista de exclusões.
---
## Testar isoladamente via Chrome DevTools performance trace
Padrão usado nesta sessão para isolar a causa do CLS: baseline → mudar UMA
variável → purge de todos os caches → novo trace → comparar.
### 1. Baseline (config actual: `ultimate-reorder` ligado, `delay=2`, sem exclusões)
```
mcp__chrome_devtools_navigate_page(url: "https://emanuelalmeida.pt/")
mcp__chrome_devtools_performance_start_trace(reload: true, autoStop: true)
# aguardar conclusão do trace (autoStop com timeout suficiente para cobrir o delay de 2s)
mcp__chrome_devtools_performance_stop_trace()
mcp__chrome_devtools_performance_analyze_insight(insightName: "CLSCulprits") # ou "LayoutShifts"
```
Registar o CLS reportado e os elementos culpados (`culprits`) — normalmente
identifica o selector/elemento exacto que salta.
### 2. Alterar a variável a testar (via SSH, fora do browser)
```bash
# Opção A — desligar o módulo por completo (teste mais bruto, isola se é o WP Meteor)
sudo -u USER /usr/local/bin/wp option patch update wp-meteor-settings ultimate-reorder enabled false --format=json --path=$PATH
# Opção B — manter ligado mas excluir os scripts suspeitos (teste mais cirúrgico)
sudo -u USER /usr/local/bin/wp option patch update wp-meteor-settings exclude value '["font-awesome","elementor-pro"]' --format=json --path=$PATH
```
### 3. Purgar TODOS os caches antes de repetir o trace
O HTML/JS servido está em pelo menos duas camadas de cache — sem purgar
ambas, o browser continua a ver a versão antiga:
```bash
# WP Fastest Cache
sudo -u USER /usr/local/bin/wp cache flush --path=$PATH
# (ou, se o WPFC não expõe flush nativo por wp-cli, apagar via admin/plugin action)
# Cloudflare (App for Cloudflare® — purge via wp-admin ou API directa da zona)
```
### 4. Repetir o trace e comparar
```
mcp__chrome_devtools_navigate_page(url: "https://emanuelalmeida.pt/")
mcp__chrome_devtools_performance_start_trace(reload: true, autoStop: true)
mcp__chrome_devtools_performance_stop_trace()
mcp__chrome_devtools_performance_analyze_insight(insightName: "CLSCulprits")
```
Comparar o valor de CLS e os `culprits` antes/depois. Se o CLS cair de
forma consistente com o módulo desligado (opção A) mas os `culprits`
apontarem para os mesmos elementos com exclusões configuradas (opção B),
confirma que o `ultimate-reorder` é a causa e que a exclusão cirúrgica
resolve sem perder o ganho de TBT nos restantes scripts.
### 5. Reverter se o teste não resolver
Se o CLS não mudar com o módulo desligado, **reverter imediatamente** ao
estado original (não deixar o site sem o ganho de performance do WP Meteor
por um teste que não confirmou a hipótese):
```bash
sudo -u USER /usr/local/bin/wp option patch update wp-meteor-settings ultimate-reorder enabled true --format=json --path=$PATH
sudo -u USER /usr/local/bin/wp option get wp-meteor-settings --format=json --path=$PATH # confirmar
```
---
## Interface admin — localização e separadores (`backend/views/admin.php`)
`wp-admin` → **Definições → WP Meteor**
(`options-general.php?page=wp-meteor`, capacidade `manage_options`).
Página com 3 separadores fixos (não há mais nenhum, confirmado no template):
| Separador (UI) | Slug interno (`tab`) | Módulo(s) renderizado(s) aqui |
|---|---|---|
| Settings | `ultimate` | `ultimate-reorder` |
| Exclusions | `exclusions` | `exclude`, `gdpr` |
| Elementor | `elementor` | `elementor-animations`, `elementor-pp` |
O módulo interno `Compatibility` não tem `$tab` definido — nunca aparece
em nenhum separador, coerente com não ser configurável pelo utilizador.
---
## Mecanismos de bypass/auto-desactivação automáticos (confirmado em `frontend/Base.php` + `backend/Enqueue.php`)
O plugin desliga-se a si próprio (não faz rewrite de scripts nessa
página/pedido) nas seguintes condições, **sem qualquer intervenção manual
necessária**:
- Query string com `wpmeteordisable` em qualquer posição — desactiva o
plugin nesse pedido (útil para QA sem alterar a option: `?wpmeteordisable=1`).
- Query string com `wpmeteordebug` — mantém o plugin activo mas carrega a
variante `public-debug.js` (não minificada, com logging) em vez de
`public.js`.
- `User-Agent` a corresponder a `/fastpixel/i`.
- `Referer` igual a `https://www.squirrly.co`.
- Constante `NITROPACK_VERSION` definida — **incompatibilidade
declarada**, dispara aviso de erro admin permanente.
- Constante `FASTPIXEL_VERSION` definida — idem.
- WP Rocket activo (`WP_ROCKET_VERSION`) **e** a opção `delay_js` do WP
Rocket está ligada — o WP Meteor desliga-se para não duplicar o
mecanismo, com aviso admin a pedir para desligar um dos dois.
- Pedido não é frontend (admin, ajax, cron, REST) — via `Is_Methods::is_frontend()`.
- Filtro `wpmeteor_enabled` devolve `false` (hook para terceiros/temas
desligarem programaticamente).
- Parâmetros GET/POST de modo de edição de **19 page builders** conhecidos:
`bricks`, `brizy-edit-iframe`, `builder` (Fusion), `ct_builder`
(Oxygen), `elementor-preview`, `et_fb` (Divi), `fb-edit` (Fusion),
`fl_builder` (Beaver Builder), `preview` (Gutenberg/block editor),
`tb-preview` (Themify), `tve` (Thrive), `uxb_iframe` (Flatsome UX
Builder), `vc_action`/`vc_editable`/`vcv-action` (WPBakery), `wyp_mode`/
`wyp_page_type` (YellowPencil), `zionbuilder-preview` (Zion Builder).
- Endpoint AMP activo (`is_amp_endpoint()` ou `ampforwp_is_amp_endpoint()`).
- Modo editor/preview activo detectado directamente via classe do builder
(não só GET/POST): Elementor (`Plugin::$instance->editor`/`preview`),
Beaver Builder (`FLBuilderModel::is_builder_active()`), WPBakery
(`vc_is_inline()`), Divi (`et_core_is_builder_used_on_current_request()`),
Avada/Fusion Builder (`Fusion_App::get_instance()->is_builder`).
- Content-Type da resposta diferente de `text/html` (ex.: JSON, XML,
feeds) — nunca reescreve respostas não-HTML.
- Página `wp-login.php`.
Isto é relevante para diagnóstico: se um script continuar "não atrasado"
mesmo com o módulo `ultimate-reorder` ligado e sem exclusão manual,
confirmar primeiro se a página em causa não cai numa destas condições de
bypass antes de suspeitar de bug/exclusão em falta.
---
## Endpoint REST `wpmeteor/v1/detect` (`rest/Marketing.php`)
`POST /wp-json/wpmeteor/v1/detect` — sem autenticação declarada no
registo da rota (`register_rest_route`, sem `permission_callback`
restritivo visível). Recebe `{"data": [...]}` do JS de admin/frontend,
junta ao array `detected` já gravado na option, corta para os últimos 20
valores (`array_slice(..., -20)`) e grava de volta.
Uso conhecido: o JS reporta strings como `"marketo"` ou `"hubspot"`
quando detecta esses formulários na página; se `detected` contém um destes
valores, `backend/Enqueue.php` mostra um aviso admin permanente sugerindo
contacto com o autor do plugin (`alex@excitingstartup.com`) para
optimizações adicionais específicas dessas ferramentas — **não é uma
funcionalidade que o utilizador configure**, é telemetria/marketing do
próprio plugin.
```bash
# Ler o campo detected (se existir) directamente da option
sudo -u USER /usr/local/bin/wp option get wp-meteor-settings --format=json --path=$PATH | jq '.detected'
```
---
## Comandos WP-CLI próprios — confirmação: NÃO existem
Procurado `WP_CLI::add_command` / `\WP_CLI` em todo o código-fonte do
plugin (`grep -rl 'WP_CLI'` em todos os `.php`) — **zero resultados**. A
única referência a `WP_CLI` no plugin é em `engine/Is_Methods.php`, e é
apenas a verificação `defined('WP_CLI') && WP_CLI` para detectar se o
pedido actual corre em contexto de CLI (usado internamente para decidir
que classes carregar), **não** um comando registado. `wp help meteor` ou
qualquer variante não existe — toda a interacção via linha de comandos é
através dos comandos genéricos `wp option get`/`wp option patch` sobre
`wp-meteor-settings`, como documentado nas secções acima.
---
## Mecânica interna do rewrite de `<script>` (para debugging avançado, `UltimateReorder.php::frontend_rewrite`)
Quando `ultimate-reorder.enabled=true`, o buffer HTML de saída é
reescrito via `ob_start()` (`frontend/Rewrite.php::buffer_start`), e cada
`<script>` sofre uma destas transformações:
- Scripts inline são temporariamente substituídos por um delimitador
único (`WPMETEOR` + password aleatória de 16 chars) para não serem
reprocessados por engano por regex subsequentes que corram sobre JSON
inserido por outros scripts — só no fim são recolocados no buffer.
- `src="..."` passa a `data-wpmeteor-src="..."` (o browser não carrega o
recurso até o JS do plugin trocar o atributo de volta).
- `type="text/javascript"` (ou ausência de `type`, ou `module`) passa a
`type="javascript/blocked" data-wpmeteor-type="..."` — o browser
reconhece `type` desconhecido e não executa o script.
- `onload=`/`onerror=` em `<html|body|img|iframe>` são envolvidos para
disparar `window.dispatchEvent(new CustomEvent('fpo:element-loaded', …))`
antes do handler original, permitindo ao motor do plugin reagir a
carregamento de imagens/iframes mesmo com scripts atrasados.
- Atributo de escape manual por tag: `data-wpmeteor-nooptimize="true"`
num `<script>` específico impede que **esse** script seja atrasado,
independentemente de exclusões globais — mecanismo usado pelo próprio
plugin para o seu script de bootstrap injectado, mas também disponível
para debugging manual (adicionar o atributo directamente no HTML de um
script problemático, via filtro `the_content` ou tema, para testar sem
tocar na option).
- Scripts já marcados por outros plugins como `data-src=`,
`data-wpmeteor-type=`, `data-pmdelayedscript=` ou
`data-rocketlazyloadscript=` são ignorados (evita duplo-delay).
O filtro `wpmeteor_exclude` (booleano, `$exclude, $content` → bool) é o
ponto de extensão central — tanto o `exclude.value` manual como as
exclusões automáticas do `gdpr` e do `Compatibility` são implementadas
como listeners deste mesmo filtro, testados por ordem de prioridade até
um devolver `true`.
---
## Gotchas / erros comuns
| Sintoma | Causa | Solução |
|---|---|---|
| Trace mostra CLS igual antes/depois de mudar a config | Cache (WPFC e/ou Cloudflare) ainda a servir HTML/JS antigo | Purgar as duas camadas de cache antes de cada trace, nunca só uma |
| `wp option patch` falha com "No data exists for key" | Caminho de chaves errado — `exclude` tem sub-chave `value`, não é directamente o array | Usar `wp option patch update wp-meteor-settings exclude value '[...]'` (dois níveis) |
| `wp plugin get wp-meteor` devolve vazio noutro site do bundle | Plugin não está instalado nesse site — confirmado em `carstuff.pt` | Nunca assumir presença; confirmar por site antes de qualquer diagnóstico |
| CLS não muda mesmo com `ultimate-reorder` desligado | Causa pode estar noutro módulo (tema, Elementor, imagens sem dimensões) | Não parar na primeira hipótese testada — este é só o próximo suspeito, não a conclusão |
| Analytics/GTM param de disparar nos primeiros segundos | `exclude.value` vazio — GA/GTM ficam sujeitos ao atraso de 2s como qualquer outro script | Adicionar `gtag`/`googletagmanager`/`gtm.js` à lista de exclusões |
| Confundir `elementor-pp` com "compatibilidade Elementor Pro" | O nome sugere isso mas o título real no código é "Emulate Elementor Powerpack Pro menu" — é sobre o plugin de terceiros Elementor PowerPack Pro (Livemesh) | Verificar se o site usa mesmo PowerPack Pro antes de investir tempo a testar este módulo como causa de um problema |
| Script continua a correr "atrasado" mesmo depois de o adicionar a `exclude.value` | Já está coberto por uma exclusão automática embutida (`gdpr` ou `Compatibility`) que corre por regex diferente do valor manual, ou a página cai numa das condições de bypass automático (builder em modo preview, WP Rocket `delay_js`, etc.) | Confirmar primeiro as secções "Exclusões automáticas embutidas" e "Mecanismos de bypass" antes de assumir que a exclusão manual falhou |
| `delay` gravado como `"-1"` e analytics/pixels param de disparar em sessões sem interacção | Comportamento esperado — `-1` significa "só corre na primeira interacção real", nunca por timeout (rdelay=86400000ms) | Se o site precisa de garantia de disparo mesmo sem interacção (bounces), usar um valor positivo, nunca `-1` |
| `wp help meteor` ou comando WP-CLI próprio não existe | O plugin não regista nenhum comando WP-CLI — confirmado por grep a todo o código-fonte | Usar sempre `wp option get/patch` sobre `wp-meteor-settings`, nunca procurar um comando dedicado |
---
## Fonte
`CONFIG-Plugins-Referencia.md` §4 (WP Meteor — mapeado 16-08-2026) +
`BUNDLE-Excelencia-WP.md` §2.2 (Performance) e tabela de pendências (linhas
218 e 233) + verificação SSH ao vivo desta sessão em `emanuelalmeida.pt`
(`wp option get wp-meteor-settings --format=json`, `wp plugin get
wp-meteor --format=json`) + **leitura completa do código-fonte do plugin**
via SSH (`find wp-content/plugins/wp-meteor -iname '*.php' | xargs wc -l`
para mapear as 5718 linhas em todos os `.php`, seguido de leitura integral
de `wp-meteor.php`, `engine/{Initialize,Base,Is_Methods}.php`,
`blocker/{Base,Event}.php`, `blocker/FirstInteraction/{Base,
UltimateReorder}.php`, `blocker/Exclusions/{Exclude,GDPR,Compatibility}.php`,
`blocker/Integration/{ElementorAnimations,ElementorPP}.php`,
`backend/{SettingsPage,SaveSettings,Enqueue,ActDeact,InstUninst}.php`,
`backend/views/admin.php`, `frontend/{Base,Rewrite}.php`,
`functions/functions.php` e `rest/Marketing.php`, na sessão de expansão de
16-08-2026). Catálogo de módulos confirmado como completo e definitivo
para `v3.4.18` — não há funcionalidades por documentar além das listadas
neste ficheiro. Nenhuma alteração foi feita em produção.