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

- emcp-page-building, emcp-content-ops, emcp-site-audit: EMCP Tools MCP
  workflows verified live (atomic/legacy interplay, apply-template overwrite
  risk, create-theme-template goes live immediately, false-positive malware
  pattern in scan-security, change ledger + rollback)
- elementor-pro-widgets: 30+5 native Elementor Pro widgets (curated catalog)
- elementskit-widgets / powerpack-widgets: 42 + 97 third-party widgets,
  widgetType extracted from plugin source (not guessed by convention)
- plugin.json bumped 1.2.0 -> 1.3.0, keywords + description updated
- commits pending wordpress skills already present as untracked files
  (emcp-tools, wordfence, wp-activity-log, seguranca-descomplicar,
  webp-express, wp-fastest-cache, wp-font-perf, wp-meteor,
  wp-activity-log, redis-object-cache, app-for-cloudflare) and pending
  edits (rank-math, wp-cli, wp-content-seo-gate)
This commit is contained in:
Claude Code
2026-08-19 03:44:00 +01:00
parent aa981e8089
commit 4a55d51329
22 changed files with 6432 additions and 29 deletions
@@ -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`.