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:
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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.*
|
||||
@@ -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).
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user