feat(wordpress): nova skill full-cliente - broker de licencas WordPress premium (api.full.services)

Cobre: auditoria de ligacao/licencas por site, ligar sites a conta FULL,
reactivacao de plugins premium, e diagnostico de licencas fora da FULL
(fabricante directo, CodeCanyon). Baseada em leitura directa do codigo do
plugin full-customer v4.1.3 durante sessao real de auditoria da frota WP.
This commit is contained in:
AIkTop
2026-08-16 15:21:49 +01:00
parent 2863d0207f
commit aa981e8089
2 changed files with 193 additions and 2 deletions
+191
View File
@@ -0,0 +1,191 @@
---
name: full-cliente
description: Gestão do plugin FULL.Cliente (broker de licenças WordPress premium, api.full.services) via WP-CLI no servidor CWP. Cobre auditoria de ligação e licenças por site, ligar/desligar sites à conta FULL, reactivação de plugins premium (Elementor Pro, Elements Kit, Rank Math Pro, etc.), e diagnóstico de licenças que NÃO passam pela FULL (fabricante directo, CodeCanyon). Usar quando "full cliente", "full.services", "plugin premium sem licença", "elementor pro expired", "ligar site à full", "reactivar licença", "quantos sites têm licença full", "quem consome esta licença".
---
# /full-cliente — Broker de licenças WordPress premium
O plugin `full-customer` (nome visível "FULL.Cliente") está instalado em cada
site WP da frota. Substitui o updater nativo de cada plugin premium que gere
por um Plugin Update Checker apontado ao servidor da FULL, e serve como
corretor de licenças compradas pela Descomplicar® — uma conta partilhada
(`it@descomplicar.pt`) por toda a frota.
**Fonte:** código lido directamente em `wp-content/plugins/full-customer/`
(v4.1.3), servidor `server.descomplicar.pt` (SSH porta 9443). Namespace REST
local: `fc/v1`. API externa: `https://api.full.services/wp-json/fc-ai`.
---
## Regra de ouro: "geridos" ≠ "comprado"
Duas fontes dizem coisas diferentes, e as duas são reais:
| Endpoint | O que mostra | Quando usar |
|---|---|---|
| `plugin-repository/updates` | Plugins com o updater ACTUALMENTE substituído pela FULL neste site | "Este plugin está a receber updates pela FULL agora?" |
| `account/licenses-extract` | Ficha real de licenças da conta: comprado/disponível/domínios em uso, por produto | "Esta licença é da FULL?" — **fonte de verdade** |
**Nunca concluir "não é FULL" só pela ausência em `updates`.** Um produto pode
estar comprado e a consumir licença em vários domínios (`licenses-extract`) e
mesmo assim estar fora do catálogo activo (`plugin-repository/all`) — o mesmo
padrão do `elementor-pro` `expired`/"retirado dos geridos". Verificar sempre
as duas antes de classificar algo como "fora da FULL". (Erro cometido e
corrigido em sessão real: "Elements Kit" tinha 13 licenças compradas, 2
disponíveis, mas estava fora do catálogo activo — parecia "sem FULL" olhando
só para `updates`.)
---
## Consultas (leitura livre, sem GATE) — sempre via `wp eval-file` seguro
Aspas aninhadas partem o PHP através de ssh. Usar sempre base64 no stdin:
```bash
B64=$(printf '%s' '<?php
// codigo PHP aqui, so aspas simples PHP normais
' | base64 -w0)
timeout 200 ssh -p 9443 -o BatchMode=yes root@server.descomplicar.pt \
"P=/opt/alt/php-fpm82/usr/bin/php; W=/usr/local/bin/wp
echo $B64 | base64 -d | \$P -d memory_limit=512M -d error_reporting=0 \$W eval-file - --allow-root --path=/home/USER/SITE 2>&1"
```
### O site está ligado?
```php
<?php
global $wpdb;
$r = $wpdb->get_row("SELECT user_id, meta_value FROM {$wpdb->usermeta} WHERE meta_key='fc/connection-email' AND meta_value!='' LIMIT 1", ARRAY_A);
echo $r ? "ligado: {$r['meta_value']} (user {$r['user_id']})" : "NAO LIGADO";
```
Atalho sem PHP: `wp option get _full_customer-connection_email --allow-root --path=$S`
— só devolve valor se o site ainda não migrou (ver secção "Porque é que um
site pode não estar ligado").
### Extracto de licenças da conta (fonte de verdade)
```php
<?php
$x = fcDashboardAPI("GET", "account/licenses-extract");
$items = isset($x["data"]["name"]) ? [$x["data"]] : ($x["data"] ?? []);
foreach ($items as $v) {
echo $v["name"] . " | comprado=" . $v["purchased"] . " | disponivel=" . $v["available"]
. " | dominios=" . implode(", ", $v["domainsInUse"] ?? []) . "\n";
}
```
Requer o site já estar ligado (usa o token do site). Corre em qualquer site
ligado, não só na conta de referência.
Para contar sites únicos com licença activa: agregar `domainsInUse` de todos
os produtos, ignorar entradas `"Site excluído"`, desduplicar por domínio.
### Catálogo activo vs geridos neste site
```php
<?php
$catalogo = fcDashboardAPI("GET", "plugin-repository/all")["data"] ?? [];
$geridos = fcDashboardAPI("GET", "plugin-repository/updates")["data"] ?? [];
echo "catalogo activo: " . count($catalogo) . " | geridos neste site: " . count($geridos) . "\n";
```
---
## Ligar um site à conta FULL (grátis — não consome licença)
Mecanismo real (`Actions/ConnectAccount.php::handleConnection`): regista o par
(email, site) no servidor FULL via `account/connect`; só se aceite grava
`fc/connection-email` no usermeta do utilizador WP que executa a acção.
**Pré-requisitos:**
- GATE 5.1 — mutação em produção. Pedir autorização
(`~/.claude/hooks/infra-auth.sh server`, válida 60 min) antes de escrever.
- Um utilizador admin local (`manage_options`). Preferir o `it@descomplicar.pt`
local se existir (fica auditável).
```bash
B64=$(printf '%s' '<?php
$ca = new \FC\Actions\ConnectAccount();
$result = $ca->handleConnection("it@descomplicar.pt");
echo json_encode($result) . "\n";
echo "isConnected: " . (\FC\User::instance()->isConnected() ? "yes" : "no") . "\n";
' | base64 -w0)
# --user=<ID do admin> e OBRIGATORIO: sem ele User::instance() nao esta
# "logged in" e o setMeta() silencia sem gravar nada (ver Erros Comuns).
echo $B64 | base64 -d | php -d memory_limit=512M wp eval-file - --user=<ID> --allow-root --path=$S
```
**Verificar sempre de forma independente**, fora da sessão do eval:
`wp user meta get <ID> fc/connection-email --allow-root --path=$S`. Não
confiar só no `success:true` da resposta.
### Porque é que um site "v4.1.3" pode não estar ligado
`Services/Connection.php::autoConnection()` corre em cada `admin_init` e tenta
migrar sozinho: lê o option legado `_full_customer-connection_email`; se tiver
valor, liga e apaga o legado (`connectionMode = 'upgrade'`). **Se esse option
já estava vazio antes da actualização do plugin, nunca houve nada para
migrar** — o site nunca esteve ligado, nem na geração antiga. Confirmar com
`wp option get _full_customer-connection_email` (vazio = nunca ligado).
---
## Reactivar a licença de um plugin (CUSTA — consome licença e abre ticket)
**Nunca fazer sem autorização explícita.** Cada `reactivate` consome uma
licença disponível e abre um ticket Zendesk do lado da FULL, que fica em fila
— não é instantâneo.
Fluxo (`Actions/PluginReactivate.php`):
1. `plugin-repository/<slug>/reactivate` — consome a licença, abre ticket;
2. `plugin-repository/<slug>/license` — busca a chave;
3. `LocalLicenseProcessor::process($slug, $license, $step, $state)` — aplica
localmente (multi-step para alguns);
4. `plugin-repository/<slug>/license/confirm` + `do_action('fc/updates/invalidate')`.
**Processador por produto (`Services/LocalLicenseProcessor.php`) — nem todos
são one-shot:**
| Produto (slug) | Fluxo |
|---|---|
| `elementorPro` | **OAuth multi-step** com a Elementor: `fetch_consent` → `submit_consent` → `finalize`, cookies de sessão entre passos. Não dá por WP-CLI puro. |
| `essentialAddons` | OTP (`check_otp`) |
| `astra`, `rankMath`, `seoPress`, `perfmatters`, `acfPRO`, `wpRocket`, `jetThemeCore`, `ultimateAddons` | Uma chamada, sem steps |
---
## Não existe forma de libertar uma licença de um domínio
Procurado exaustivamente no código (`grep -rniE "release|remove.?domain|deactivate.?domain|revoke|free.?license"`)
— **zero mecanismo**. `Actions/DisconnectAccount.php` só desliga a ligação
LOCAL (`setConnectionEmail('')`); o próprio texto confirma: "suas ativações e
licenças seguem funcionando normalmente". As únicas acções por produto
(`PluginActivationManager.php`) são "Reativar", "Renovar" e "Solicitar ajuda"
— e "ajuda" é acção de frontend, sem rota REST própria. Para libertar um
domínio (ex.: site descomissionado que ainda consome uma licença comprada), a
única via é abrir um pedido de suporte à FULL — não há atalho técnico.
---
## Licenças que NÃO são da FULL (não confundir)
| Fonte | Como identificar | Exemplo real |
|---|---|---|
| **Fabricante directo** | Option própria com `license_key`, formato do produto (não FULL) | `elementskit_options.license_key` (WPMET), `_ff_fluentform_pro_license_key` (WPManageNinja) |
| **CodeCanyon (compra única)** | Zero option de licença, zero `envato`/`purchase_code` no código — ficheiro entregue sem phone-home, por desenho | `kivicare-pro` |
Confirmar sempre as duas possibilidades (FULL via `licenses-extract` E licença
própria do fabricante) antes de classificar um plugin como "sem licença" — um
produto pode estar em ambas, nenhuma, ou só numa.
---
## Erros comuns
| Sintoma | Causa | Solução |
|---|---|---|
| "X não é FULL" (conclusão errada) | Só verificado em `plugin-repository/updates`, faltou `account/licenses-extract` | Verificar sempre as duas fontes (ver "Regra de ouro") |
| `ConnectAccount` devolve `success:true` mas usermeta continua vazio | Faltou `--user=<ID>` no `wp eval-file` — `User::instance()` não estava "logged in", `setMeta()` silencia | Sempre passar `--user=<ID admin>` |
| Erro de sintaxe PHP no `eval-file` | Aspas duplas aninhadas via ssh | Base64 no stdin (ver topo desta skill) |
| Domínio "morto" que na verdade está vivo | DNS aponta para o servidor mas é conta partilhada com outros sites | Confirmar `siteurl` real (`wp option get siteurl`) antes de declarar morto — nunca só presença/ausência de pasta |