diff --git a/wordpress/.claude-plugin/plugin.json b/wordpress/.claude-plugin/plugin.json index e9485e6..483ec07 100644 --- a/wordpress/.claude-plugin/plugin.json +++ b/wordpress/.claude-plugin/plugin.json @@ -1,12 +1,12 @@ { "name": "wordpress", "description": "WordPress development, maintenance and optimization - plugins, themes, WooCommerce, Elementor, Crocoblock. Backed by NotebookLM notebooks.", - "version": "1.1.0", + "version": "1.2.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"] + "keywords": ["wordpress", "woocommerce", "elementor", "crocoblock", "development", "performance", "licensing"] } diff --git a/wordpress/skills/full-cliente/SKILL.md b/wordpress/skills/full-cliente/SKILL.md new file mode 100644 index 0000000..b116318 --- /dev/null +++ b/wordpress/skills/full-cliente/SKILL.md @@ -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' '&1" +``` + +### O site está ligado? + +```php +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 +handleConnection("it@descomplicar.pt"); +echo json_encode($result) . "\n"; +echo "isConnected: " . (\FC\User::instance()->isConnected() ? "yes" : "no") . "\n"; +' | base64 -w0) +# --user= 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= --allow-root --path=$S +``` + +**Verificar sempre de forma independente**, fora da sessão do eval: +`wp user meta get 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//reactivate` — consome a licença, abre ticket; +2. `plugin-repository//license` — busca a chave; +3. `LocalLicenseProcessor::process($slug, $license, $step, $state)` — aplica + localmente (multi-step para alguns); +4. `plugin-repository//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=` no `wp eval-file` — `User::instance()` não estava "logged in", `setMeta()` silencia | Sempre passar `--user=` | +| 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 |