Especificação funcional do plugin emcp-tools v3.12.1 (msrbuilds/elementor-mcp, GPL-2.0-or-later) via leitura directa do código-fonte em emanuelalmeida.pt. - 00: arquitectura (bootstrap, ability registrar, dispatcher, MCP adapter) - 01: Elementor classico (paginas, layout, widgets, templates, globals) - 02: Elementor Atomic v4 + Gutenberg - 03: WordPress core (conteudo, media, settings, temas) - 04: Themer (CPT, condicoes, render, PHP templates) - 05: Redirects + change ledger unificado (rollback) - 06: Sandbox PHP snippets + custom widgets - 07: Filesystem/DB/WP-CLI/Security/Performance (maior risco) - 08: Integracoes terceiros (ACF, Meta Box, forms, SEO) - 09: Stock images + Cloud + OAuth - 10: Sistema de modulos + inventario Pro-only (30 classes) - INDEX: sintese, sequencia de construcao, tabela de risco Produzido por 10 subagentes code-explorer em paralelo + revisao cruzada de consistencia. Cada doc inclui blueprint de replica (copiar/simplificar/omitir).
35 KiB
06 — Sandbox de PHP Snippets e Infraestrutura de Widgets/Blocos Custom
Este documento mapeia o subsistema "Sandbox" do EMCP Tools: a funcionalidade Free de PHP Snippets criados por agentes de IA com aprovação humana obrigatória, o contrato de export/import cross-artefacto (EMCP_Tools_Sandbox_Artifact), a infraestrutura de caminhos partilhada, e o Widget Builder (Pro) — cuja parte MCP está ausente desta build Free, deixando apenas a camada de armazenamento/admin acessível a partir do wp-admin. Documenta-se também class-mcpb-builder.php, cujo nome sugere parentesco com o Widget/Block Builder mas que é, na realidade, uma feature totalmente distinta (gerador de bundle Claude Desktop .mcpb).
1. Ability: EMCP_Tools_PHP_Snippet_Abilities
Ficheiro: includes/abilities/class-php-snippet-abilities.php
Condição de registo (class-ability-registrar.php, linhas ~416-421):
// PHP Snippet abilities (Sandbox) — free, capability-gated, no Elementor.
if ( class_exists( 'EMCP_Tools_PHP_Snippet_Abilities' ) ) {
$php_snippets = new EMCP_Tools_PHP_Snippet_Abilities();
$php_snippets->register();
...
}
A classe existe incondicionalmente na build Free — não há gate de Pro nem de Elementor. O gate real é feito por-tool via permission_callback, apoiado em duas capabilities WordPress nativas: manage_options (leitura) e manage_options + unfiltered_html (escrita — as mesmas capabilities que já permitem a um utilizador editar código de plugin).
| Ability | Input (resumo) | O que faz | Permission callback | readonly / destructive |
|---|---|---|---|---|
validate-php-snippet |
code (string, obrigatório) |
Verifica estaticamente o código SEM o guardar nem correr: confirma que faz parse e depois corre o scanner de segurança (execução de código, shell, escritas de ficheiro, rede, ofuscação, SQL destrutivo). Devolve relatório {valid, safe, parse_error, findings[]}. Pensado para iterar antes de create-php-snippet. |
check_read_permission → manage_options |
readonly=true, destructive=false, idempotent=true |
create-php-snippet |
title?, code (obrigatório), context? (enum shortcode|hook|both), hook?, priority? |
Cria o snippet como DRAFT INACTIVO. Nunca corre. Valida primeiro; rejeita com invalid_php (erro de parse) ou unsafe_php (finding crítico) devolvendo o relatório de validação para o agente corrigir. |
check_edit_permission → manage_options AND unfiltered_html |
readonly=false, destructive=false, idempotent=false |
update-php-snippet |
snippet_id (obrigatório) + campos de create-php-snippet (parciais) |
Actualiza código/config. Re-valida (mesmas rejeições). Se o snippet já estiver activo, recompila o executável em disco; se a recompilação falhar, degrada automaticamente para draft. A activação continua a exigir um passo humano separado. | check_edit_permission |
readonly=false, destructive=false, idempotent=false |
get-php-snippet |
snippet_id (obrigatório) |
Devolve o registo completo: código, status (draft/active), contexto de execução, shortcode gerado, e o último relatório de validação. | check_read_permission |
readonly=true, idempotent=true |
list-php-snippets |
status? (enum active|draft|any) |
Lista snippets com status, contexto e shortcode. Devolve {count, snippets[]}. |
check_read_permission |
readonly=true, idempotent=true |
delete-php-snippet |
snippet_id (obrigatório) |
Apaga permanentemente o snippet (post CPT + ficheiro sandbox, se existir). | check_edit_permission |
readonly=false, destructive=true, idempotent=false |
Decisão de design central (citação directa do topo do ficheiro): "Lets an AI agent author, validate, read, and manage PHP snippets — but NEVER run them. There is intentionally no 'activate' tool: a snippet created via MCP is an inactive draft until a human administrator reviews it and activates it in the Sandbox admin screen."
Helper normalize_write_result(): transforma um WP_Error de rejeição de validação (invalid_php/unsafe_php) numa resposta estruturada {success:false, reason, validation} — em vez de um erro opaco, o agente recebe o relatório de findings completo para poder corrigir o código e tentar de novo. Em sucesso, acrescenta sempre uma note a lembrar que o draft precisa de aprovação de um administrador.
2. Ability: EMCP_Tools_Sandbox_Cloud_Abilities
Ficheiro: includes/abilities/class-sandbox-cloud-abilities.php
Condição de registo (class-ability-registrar.php, linhas ~429-434):
// Sandbox cloud export/import (free; operates over the bundle contract).
if ( class_exists( 'EMCP_Tools_Sandbox_Cloud_Abilities' ) ) {
$cloud = new EMCP_Tools_Sandbox_Cloud_Abilities();
$cloud->register();
...
}
Sempre registada na build Free (ao contrário do Widget Builder, esta classe não se autoguarda por Pro). O gate por-kind acontece dentro de resolve_artifact(), não na classe.
| Ability | Input (resumo) | O que faz | Permission callback | readonly / destructive |
|---|---|---|---|---|
export-sandbox-artifact |
kind (enum block|widget|snippet, obrigatório), id (int, obrigatório) |
Exporta um artefacto de sandbox como bundle portátil, verificado por checksum, pronto para partilha/sincronização cloud. Devolve {bundle: object}. |
current_user_can('manage_options') |
readonly=true, destructive=false, idempotent=true |
import-sandbox-artifact |
bundle (object, obrigatório — produzido por export-sandbox-artifact) |
Importa um bundle como um novo draft local. O bundle é validado (versão de schema, checksum) antes de qualquer escrita. Devolve {id: int}. |
current_user_can('manage_options') |
readonly=false, destructive=false, idempotent=false |
Resolução de artefacto (resolve_artifact($kind)):
switch ( $kind ) {
case 'block': return class_exists('EMCP_Tools_Block_Store') ? EMCP_Tools_Block_Store::instance() : null; // Pro-only, ausente nesta build
case 'widget': return new EMCP_Tools_Widget_Bundle_Adapter(); // sempre disponível
case 'snippet': return new EMCP_Tools_Snippet_Bundle_Adapter(); // sempre disponível
default: return null;
}
Quando resolve_artifact() devolve null para um kind válido (block), a ability devolve um WP_Error pro_required em vez de fatal — falha limpa. Nota importante: mesmo com kind=widget resolúvel (o adapter existe sempre), a operação real de import ainda pode falhar internamente com WP_Error('forbidden', ...) dentro de EMCP_Tools_Widget_Store::create() se a licença Pro não estiver activa — o gate real vive na store, não no resolver.
3. Serviços de suporte
3.1 EMCP_Tools_PHP_Snippet_Store — includes/class-php-snippet-store.php
Storage: CPT privado emcp_php_snippet (public=false, show_in_rest=false, capability_type=page, map_meta_cap=true).
Meta keys: _emcp_snippet_code (código raw, wp_slash()-eado), _emcp_snippet_context, _emcp_snippet_hook, _emcp_snippet_priority, _emcp_snippet_validation (JSON do relatório), _emcp_snippet_hash (sha256 do ficheiro compilado), _emcp_snippet_error.
Post status = flag de activação: publish = activo, draft = inactivo. Este é o único mecanismo de "ligar/desligar" um snippet.
Ficheiro em disco: wp-content/emcp-sandbox/snippets/{id}.php — só existe enquanto o snippet está activo. Um draft não tem artefacto executável em disco. O ficheiro é gerado por write_executable(): o código é despido de tags PHP (strip_tags()), embrulhado numa função única (emcp_php_snippet_{id}()), e o resultado final passa por um token_get_all(..., TOKEN_PARSE) de segurança (guarda final antes de escrever).
Manifest: wp-content/emcp-sandbox/snippets-manifest.json — array de {post_id, func, php_path, hash, context, hook, priority} apenas dos snippets publish. Reconstruído (rebuild_manifest()) após qualquer create/update/set_status/delete/mark_error.
Permissões:
can_edit():manage_optionsANDunfiltered_htmlcan_read():manage_options
Fluxo CRUD:
create_draft()— sempre cria emdraft; valida primeiro e rejeita cominvalid_php/unsafe_php(WP_Error com o relatório emerror_data['validation']).update()— re-valida; se o snippet já estava activo, chamawrite_executable()de novo; se a escrita falhar, degrada automaticamente para draft e regista o erro.set_status('active'|'draft')— é o portão de aprovação humana. Nunca é chamado pelas MCP abilities (só pelo handler AJAX do admin). Ao activar: re-valida (bloqueia comactivation_blockedse inválido/inseguro), escreve o executável, muda parapublish. Ao desactivar: apaga o ficheiro, apaga o hash, muda paradraft.mark_error()— chamado pelo loader quando um snippet crasha em runtime: desactiva automaticamente, regista o erro, reconstrói o manifest.uninstall_cleanup()— apaga todos os posts + omanifest.jsonno desinstalar do plugin.
3.2 EMCP_Tools_PHP_Snippet_Loader — includes/class-php-snippet-loader.php
Corre em plugins_loaded, regista o shortcode [emcp_snippet id="N"] e carrega os snippets activos.
load() é manifest-only (nunca faz scan de directório). Para cada entrada do manifest:
- Path containment:
0 !== strpos(normalize(path), normalize(sandbox))→ salta (defende contra manifest envenenado). - Tamper guard: recalcula
hash('sha256', file_get_contents($path))e compara com o hash registado → salta se não bater certo. include_oncedo ficheiro — isto só define a função, não executa código do utilizador.- Se
contextforhook/both, fazadd_action($hook, closure, $priority)que despoletarun_on_hook().
Execução:
render_shortcode()— só corre secontextforshortcode/both; captura output viaob_start(); se a função devolver string/numérico, é concatenado ao output do buffer.run_on_hook()— corre a função directamente no hook (output vai inline, ex.:wp_footer).
Isolamento de falhas — camada dupla:
try { ... } catch (\Throwable $e)em cada execução.register_shutdown_functioncomo rede de segurança para os fatais que umtry/catchnão apanha (E_ERROR,E_PARSE,E_CORE_ERROR,E_COMPILE_ERROR,E_USER_ERROR) — se um snippet crasha (mesmo em erro de parse do próprio ficheiro incluído),mark_error()desactiva-o automaticamente para a request seguinte recuperar. Um snippet mau não consegue white-screenar o site de forma persistente.
3.3 EMCP_Tools_PHP_Snippet_Validator — includes/class-php-snippet-validator.php — SECÇÃO CRÍTICA
Duas camadas de validação:
Camada 1 — PARSE. O código é embrulhado exactamente como vai correr: <?php function __emcp_snippet_validate() { CODE\n} — e passa por token_get_all($wrapped, TOKEN_PARSE). Um \ParseError/\Throwable aqui devolve valid=false com a mensagem de erro, sem sequer chegar ao scan de segurança.
Camada 2 — SECURITY SCAN. Percorre os tokens significativos (whitespace/comments removidos) e aplica regras por token e por vizinhança (prev/next).
Bloqueio CRÍTICO por nome de função (mapa severity → reason, bloqueia create/activate):
| Categoria | Funções |
|---|---|
| Execução de código arbitrário | eval, assert, create_function |
| Shell / processo | exec, system, shell_exec, passthru, proc_open, popen, pcntl_exec, expect_popen |
| Invocação dinâmica (bypassa este próprio check) | call_user_func, call_user_func_array, forward_static_call, forward_static_call_array, func_get_args |
| Escritas/apagamentos de ficheiro | file_put_contents, fwrite, fputs, fputcsv, ftruncate, unlink, rmdir, rename, copy, mkdir, chmod, chown, chgrp, symlink, link, move_uploaded_file |
| Rede | curl_init, curl_exec, curl_setopt, fsockopen, pfsockopen, stream_socket_client, socket_create, socket_connect |
| Decoders de ofuscação (sinal nº1 de malware) | base64_decode, gzinflate, gzuncompress, gzdecode, str_rot13, convert_uudecode, hex2bin |
| Runtime/ambiente | dl, putenv, ini_set, ini_alter, apache_setenv, virtual, set_error_handler, register_shutdown_function, register_tick_function, extract |
Bloqueio CRÍTICO por construto de linguagem (análise de tokens, não lista de nomes):
- Backtick shell execution —
`...` include/include_once/require/require_once— bloqueia sempre, mesmo estático ("loads and runs another PHP file")T_EVAL— construto de linguagemeval, além da função- Chamada de função por variável —
$var(...)— "Calls a function named by a variable (bypasses static checks)" - Instanciação dinâmica —
new $var— classe escolhida em runtime - Reflection/Closure factories —
new ReflectionFunction/ReflectionMethod/ReflectionClass/ReflectionObject/Closure - Tag de fecho PHP embutida —
?>— bloqueia porque permitiria "escapar" do wrapper para output HTML cru - SQL destrutivo dentro de string literal — regex
/\b(DROP|TRUNCATE|ALTER)\s+(TABLE|DATABASE)\b/iou/\bDELETE\s+FROM\b/i
Apenas AVISO (warning, não bloqueia; fica visível ao revisor humano na UI):
- Leitura de ficheiros:
fopen,file_get_contents,readfile,fread,fgets,scandir,glob,opendir define,header,setcookie,error_reporting- WordPress sensível:
update_option,delete_option,add_option,wp_mail,wp_delete_post,wp_delete_user,wp_insert_user,wp_update_user,switch_theme,activate_plugin,deactivate_plugins,do_action - Callbacks dinâmicos — o vector clássico de bypass da lista crítica acima (ex.:
array_map('system', $_GET['cmds'])evitaria a checagem directa desystem):array_map,array_filter,array_walk,array_walk_recursive,array_reduce,usort,uasort,uksort,ob_start,preg_replace_callback,preg_replace_callback_array,set_exception_handler,iterator_apply die/exit(T_EXIT)- Variável variável (
$$x) - Supressão de erro (
@) - Superglobais:
$_GET,$_POST,$_REQUEST,$_FILES,$_COOKIE,$_SERVER,$_ENV,$GLOBALS - Definição de
function/class/trait/interfacedentro do snippet (risco de redeclaração fatal em re-execuções)
strip_tags()
Remove um único <?php (ou <?=/<?) inicial via regex, para aceitar código com ou sem tags de abertura — usado tanto na validação como na compilação final.
Postura de segurança declarada (citação directa do topo do ficheiro):
"IMPORTANT — this is a GUARDRAIL, not a guarantee. PHP is expressive enough to hide intent (variable functions, decoded strings, reflection), so static analysis cannot prove arbitrary code is safe. The real safety boundary is the capability gate (manage_options + unfiltered_html) plus the human approval step: an AI can create a DRAFT and run the validator, but only an admin can activate a snippet so it actually executes."
Esta mensagem é reforçada literalmente na UI de administração (admin/views/sandbox/snippets.php): "The validator blocks obviously dangerous code (...), but static analysis is a guardrail, not a guarantee, only activate code you have read and trust. Activation is the approval step; AI can only create inactive drafts."
3.4 EMCP_Tools_Sandbox_Paths — includes/sandbox/class-sandbox-paths.php
Centraliza todos os caminhos de sandbox (usado por snippets, widgets, blocks Pro, theme-php).
- Base dir:
wp-content/emcp-sandbox(nome filtrável viaemcp_tools_sandbox_folder, caminho absoluto filtrável viaemcp_tools_sandbox_dir, URL viaemcp_tools_sandbox_url). - Localização legada:
wp-content/uploads/emcp-widgets— antes de a v3.7 introduzir a pasta única sobwp-content/, os artefactos viviam dispersos sob uploads. maybe_migrate()— migração automática one-time, corre no bootstrap (plugins_loaded), antes dos loaders (init). Tentarename(); se falhar (device diferente), fazcopy_tree()+rmdir_tree()como fallback. Guarda o flag emoption('emcp_tools_sandbox_location')para nunca repetir. Insight de design: os manifests guardam caminhos relativos + hashes de conteúdo — por isso a migração nunca precisa de reconstruir um único manifest, todos os hashes continuam válidos contra a nova base.harden()— escreve umindex.phpde silêncio + um.htaccessque bloqueia execução directa de.php(<FilesMatch "\.php$"> Require all denied) mas continua a permitir servir.css/.jsestáticos.guard_subdir()— garanteindex.phpde silêncio em cada subpasta nova.relative_base()— caminho relativo aABSPATH, usado pelo scanner de malware (verskill://emcp-tools, já auditada) para excluir o próprio PHP sandboxado do plugin da verificação de malware.
3.5 EMCP_Tools_Sandbox_Bundle — includes/sandbox/class-sandbox-bundle.php
Envelope de portabilidade partilhado por todos os kinds (block, widget, snippet).
SCHEMA_VERSION = 1,KINDS = ['block', 'widget', 'snippet'].build()— monta{schema_version, kind, uuid, meta, spec, assets, version, updated_at, checksum}.checksum()—ksort($assets)seguido desha256(wp_json_encode($assets))— determinístico, prefixadosha256:.validate()— validaschema_version(1..SCHEMA_VERSION),kindconhecido, presença de todas as chaves obrigatórias,assetsé array, e recalcula o checksum e compara — devolveWP_Error('bundle_checksum', ...)("tampered or corrupt") se não bater certo.
3.6 EMCP_Tools_Sandbox_Store (abstract) — includes/sandbox/class-sandbox-store.php
Classe-base comum para stores tipo-artefacto, implementa EMCP_Tools_Sandbox_Artifact parcialmente.
- Meta keys partilhadas:
_emcp_uuid,_emcp_origin,_emcp_remote_id,_emcp_sync_state,_emcp_version,_emcp_updated_at. ensure_uuid()/uuid()— gera UUID4 uma vez, persiste.bump_version()— incrementa versão, marcasync_state='dirty', registaupdated_at.sync_meta()— pacote de metadados de sincronização cloud.artifact_dir()/artifact_url()— caminho e URL por artefacto (útil para enfileirar assets próprios de um artefacto, ex. script de editor de um bloco, cujo URL o resolverfile:deblock.jsondo WordPress não consegue calcular para uma sandbox fora de um plugin/tema).- Helpers I/O partilhados:
write_file/read_file/delete_file/rmdir_recursive(com invalidação de opcache em escritas.php).
Nota de arquitectura: apesar de existir, esta classe abstract não é usada pelo EMCP_Tools_Widget_Store — que reimplementa manualmente os mesmos padrões (write_file/read_file/rmdir_recursive/sync-like meta). É consumida pelo Block Store (Pro, ausente desta build). A explicação plausível: o Widget Store é @since 1.9.0 (anterior), esta classe sandbox/ é claramente da geração @since 3.7.0 dos adapters cloud — dívida técnica de evolução do produto, não um erro.
3.7 Adapters — class-snippet-bundle-adapter.php e class-widget-bundle-adapter.php
Ambos implementam EMCP_Tools_Sandbox_Artifact sem tocar no store subjacente — thin adapters puros.
EMCP_Tools_Snippet_Bundle_Adapter |
EMCP_Tools_Widget_Bundle_Adapter |
|
|---|---|---|
assets() |
{code.php: código raw NÃO compilado} — deliberadamente não o executável envolvido em função (esse é maquinaria local do store, re-derivada em cada import) |
{widget.php, style.css?, script.js?} — os ficheiros gerados (compilados) |
to_bundle() |
Monta via EMCP_Tools_Sandbox_Bundle::build('snippet', ...) |
Monta via EMCP_Tools_Sandbox_Bundle::build('widget', ...), usando EMCP_Tools_Widget_Store::get_spec() como spec |
apply_bundle() |
Chama EMCP_Tools_PHP_Snippet_Store::create_draft() — sempre novo draft, nunca activa |
Chama EMCP_Tools_Widget_Store::create($spec, false) — $active=false explícito, mesmo princípio |
Ambos gravam _emcp_uuid = bundle.uuid e _emcp_origin = 'imported' no post recém-criado. O portão de aprovação humana é preservado mesmo no fluxo de import — importar um bundle nunca substitui a necessidade de um administrador activar o resultado.
3.8 interface EMCP_Tools_Sandbox_Artifact — includes/sandbox/interface-sandbox-artifact.php
Contrato mínimo, consumido por EMCP_Tools_Sandbox_Cloud_Abilities::resolve_artifact():
interface EMCP_Tools_Sandbox_Artifact {
public function kind(): string;
public function uuid( int $id ): string;
public function to_bundle( int $id ); // array|WP_Error
public function apply_bundle( array $bundle ); // int|WP_Error (novo id local)
public function checksum( int $id ): string;
public function sync_meta( int $id ): array;
}
3.9 EMCP_Tools_Widget_Store — includes/class-widget-store.php (839 linhas na tarefa)
CONFIRMAÇÃO DIRECTA: este ficheiro existe na build Free (lido na íntegra via SSH). O que não existe é includes/abilities/class-widget-builder-abilities.php — ausente da listagem de includes/abilities/ neste servidor. O registrar só instancia condicionalmente:
// Widget Builder (Pro; self-guards on license).
if ( class_exists( 'EMCP_Tools_Widget_Builder_Abilities' ) ) {
$widget_builder = new EMCP_Tools_Widget_Builder_Abilities();
$widget_builder->register();
...
}
Como o ficheiro dessa classe não existe, class_exists() é sempre false nesta build → create-custom-widget, create-custom-block e afins NÃO estão registadas como MCP abilities. "Self-guards on license" refere-se ao comportamento se o overlay Pro privado estivesse presente: a própria classe verificaria a licença dentro de si. Aqui nem chega a ser definida.
Também confirmado ausente: includes/class-widget-generator.php (o compilador spec→PHP) e includes/class-block-store.php — nenhum dos dois consta da listagem de includes/ desta build. Isto confirma explicitamente a nota da própria class-sandbox-cloud-abilities.php: "the 'block' kind is Pro-only via the resolver: EMCP_Tools_Block_Store is absent on free sites".
Duas camadas de gate independentes para a funcionalidade real (mesmo hipotetizando que a classe de abilities existisse):
- Gate de licença Freemius (soft — a classe corre, devolve
false):user_has_access()→emcp_tools_fs()->can_use_premium_code() && current_user_can('manage_options'). - Gate de código ausente (hard):
write_widget()verificaclass_exists('EMCP_Tools_Widget_Generator')— o compilador spec→PHP, ficheiro Pro-only ausente desta árvore. Se ausente, devolveWP_Error('emcp_pro_required', 'Widget Builder requires EMCP Pro.')mesmo queuser_has_access()fossetrue.
O que a classe faz (arquitectura, documentada mesmo sem poder correr nesta build):
- CPT privado
emcp_widget. Meta:_emcp_spec(JSON regenerável, fonte-de-verdade),_emcp_widget_name,_emcp_class_name,_emcp_php_hash,_emcp_css_hash,_emcp_js_hash,_emcp_last_error. - Post status = activação:
publish= activo (carregado no Elementor),draft= inactivo. - Storage:
wp-content/emcp-sandbox/widgets/{id}/widget.php(+style.css/script.jsopcionais). - Nunca escreve no tema, no core ou noutros plugins — sandbox isolada, tal como os snippets (declarado no cabeçalho do ficheiro).
create(spec, active=true): insere o post primeiro (para o ID poder semear nomes únicos de classeEMCP_Widget_{id}/widgetemcp_custom_{id}), depoiswrite_widget(), e se$activechamasafeguard_active()antes de aparecer no manifest.write_widget(): chamaEMCP_Tools_Widget_Generator::generate($spec, $class_name, $widget_name, $handles)— compila a spec estruturada em PHP. O agente de IA nunca escreve PHP cru, só a spec JSON — confirmado pelo texto da própria UI admin: "these widgets are PHP compiled by this plugin from an AI-supplied spec (the AI never writes raw PHP). Output is escaped by control type."- Defesa de injecção nos assets estáticos: CSS/JS aceitam ficheiros completos opcionais, mas com
preg_replace('/<\?(?:php|=)?/i', '', $out)— garante que um.css/.jsgerado nunca pode ser executado como PHP mesmo comshort_open_tagactivo num servidor mal configurado. runtime_validate()— safeguard notável: instancia a classe gerada e chama$instance->get_stack()(forçainit_controls()→register_controls(), exactamente o que o editor Elementor faz), antes de deixar o widget ir para produção. Se rebentar, é apanhado aqui em vez de dar white-screen no painel do editor Elementor. Chamado tanto emcreate()como emset_status('active').safeguard_active(): se a validação runtime falhar, degrada automaticamente para draft e regista o erro — nunca deixa algo quebrado ficar "activo".mark_error(): chamado pelo loader em runtime (paralelo ao snippet loader) quando um widget crasha após já ter sido carregado.- Mesmo padrão manifest-only (
rebuild_manifest()/read_manifest()) dos snippets. uninstall_cleanup(): apaga todos os posts e fazrmdir_recursive(self::sandbox_dir())— a árvore sandbox inteira, não sówidgets/. Note-se que isto é diferente doPHP_Snippet_Store::uninstall_cleanup(), que só apaga os seus próprios ficheiros individuais — risco arquitectural a evitar numa réplica se vários subsistemas partilharem a mesma raiz de sandbox (aqui não causa dano porque o desinstalar do plugin apaga tudo de uma vez, mas é frágil).
3.10 EMCP_Tools_Widget_Loader — includes/class-widget-loader.php (bónus — runtime companion do Widget Store)
Não foi pedido explicitamente na lista de ficheiros, mas foi lido para fechar a compreensão do ciclo de vida completo do widget (paralelo directo ao PHP_Snippet_Loader).
has_access(): mesmo gate Freemius do Store — "The whole loader is Pro-gated: on a free/unlicensed site nothing is loaded" (comentário do próprio autor).- Regista categoria Elementor "Custom (EMCP)" (slug
emcp-custom) só sehas_access(). register_widgets(): manifest-only, tamper guard sha256, path-containment guard — padrão idêntico ao snippet loader.register_assets():wp_register_style/wp_register_scriptpor widget com handleemcp-widget-{id}-style/-script, versionados pelo hash do ficheiro (cache-busting automático em cada regeneração).- Mesma rede de segurança de shutdown handler + isolamento de fatais que o snippet loader.
3.11 EMCP_Tools_Mcpb_Builder — includes/admin/class-mcpb-builder.php — NÃO é "Widget/Block Builder"
Desambiguação explícita, confirmada por leitura directa do ficheiro: apesar de o nome "MCP Builder" sugerir parentesco com o Widget/Block Builder, esta é uma feature completamente distinta e não relacionada com o sandbox de código custom. Constrói um bundle .mcpb (formato Claude Desktop Extension) que instala um servidor MCP standalone que faz proxy para o REST API do WordPress — é o mecanismo para ligar o Claude Desktop directamente a este site sem passar por um MCP host remoto.
build_manifest(): geramanifest.jsonversão MCPB0.3. Onameé único por site, derivado do host (host_slug()), porque "Claude Desktop identifies extensions by the manifestname(not the filename ordisplay_name), so the name must be unique per site or a second install replaces the first" — comentário que referencia um bug real (#86) já reportado.mcp_config.argsusa${__dirname}/server/index.js(variável de substituição MCPB) em vez de caminho relativo — comentário do autor explica: "Claude Desktop does not cd into the extracted bundle dir before runningnode, so a relative path resolves against the wrong CWD and Node throws 'Cannot find module' → instant 'Server disconnected'".build_zip(): lêbin/mcp-proxy.mjs(fonte ESM, testável comnode --test), converte para CJS self-contained via regex simples (trocaimport ... from 'node:X'porrequire('X'), removeexport), e embute as credenciais (WP_URL,WP_USERNAME,WP_APP_PASSWORD) como overrides deprocess.envno topo do ficheiro — para funcionar mesmo que o host Claude Desktop não injectemcp_config.env.validate_server_js(): sanity check pós-build — confirma presença dos 4require()esperados e ausência de qualquerimport/exportESM residual; falha cedo em vez de distribuir um bundle partido.- Disparado pelo admin-post
emcp_tools_download_mcpb(NONCE_DOWNLOAD_MCPB, visto emincludes/admin/class-admin.php), a partir do separador "Connection" das definições do plugin — nada a ver com a página Sandbox.
4. Blueprint para réplica
Copiar quase 1:1
- O validador em 3 camadas (parse → scan de tokens → classificação critical/warning) — é o coração da segurança deste subsistema e está bem pensado: cobre tanto nomes de função óbvios como vectores de bypass (chamada dinâmica, callbacks). A lista de funções críticas está bem pesquisada; copiar quase literalmente.
- O padrão "manifest-only loading + tamper guard sha256 + path containment check" — usado tanto no snippet loader como no widget loader. É elegante e evita I/O desnecessário em cada request (nunca faz scan de directório).
- O padrão "shutdown handler + auto-deactivate on fatal" — garante que um snippet/widget mau nunca consegue white-screenar o site de forma persistente (recupera na request seguinte). Detalhe fácil de esquecer numa reescrita ingénua.
- O gate de duas camadas para escrita de PHP (
createsó pode produzir draft;activateé acção humana separada, nunca exposta via MCP) — é a decisão de design mais importante deste subsistema todo e deve ser preservada tal e qual: "There is intentionally no 'activate' tool" no MCP. - O envelope de bundle (
EMCP_Tools_Sandbox_Bundle) com checksum determinístico (ksort+sha256) — simples e eficaz para portabilidade/import-export entre sites. - A defesa de injecção de tag PHP em ficheiros CSS/JS gerados (
preg_replace('/<\?(?:php|=)?/i', '', $out)) — detalhe fácil de esquecer que evita um vector de RCE seshort_open_tagestiver activo no servidor de destino. runtime_validate()do Widget Store — instanciar e forçarget_stack()antes de activar, para nunca deixar código gerado partido chegar ao editor Elementor.
Simplificar
- A dualidade
EMCP_Tools_Sandbox_Store(abstract,@since 3.7.0) vs a implementação manual duplicada emEMCP_Tools_Widget_Store(@since 1.9.0) é dívida técnica visível — ambas fazem o mesmo (write_file/read_file/rmdir_recursive/sync-meta) com código quase idêntico. Numa reescrita, fazer o Widget Store herdar deSandbox_Storedesde o início. - A migração legada
uploads/emcp-widgets→wp-content/emcp-sandbox(EMCP_Tools_Sandbox_Paths::maybe_migrate) só é necessária porque o produto original mudou de local a meio da vida. Numa réplica de raiz, ir directo parawp-content/{slug}-sandboxsem essa bagagem.
Deixar de fora ou adiar
- O MCPB Builder (Claude Desktop
.mcpb) é uma feature auxiliar completamente ortogonal ao sandbox — só implementar se o objectivo for também suportar o cliente Claude Desktop nativo além de MCP hosts remotos. Não é "custom code sandbox", é "distribuição de credenciais de ligação". - O sistema cloud/marketplace (Save to Cloud/Publish/View on Marketplace, visto nas views admin como
EMCP_Tools_Admin::render_sandbox_cloud_actions()/render_cloud_library()) claramente pertence a outro documento (provavelmente o de integrações cloud/OAuth) — não aprofundado aqui, apenas mencionado como consumidor do contratoEMCP_Tools_Sandbox_Artifact.
Gotchas não óbvios (citações directas)
- "Claude Desktop identifies extensions by the manifest
name(not the filename ordisplay_name), so the name must be unique per site or a second install replaces the first" — referenciando o bug #86 relatado. - "Claude Desktop does not cd into the extracted bundle dir before running
node, so a relative path resolves against the wrong CWD" — motivo de usar${__dirname}. - "PHP is expressive enough to hide intent (variable functions, decoded strings, reflection), so static analysis cannot prove arbitrary code is safe" — o autor é honesto sobre os limites do validador, não vende segurança que não tem.
EMCP_Tools_Widget_Store::uninstall_cleanup()apagasandbox_dir()inteiro (não sówidgets/) — cuidado ao portar esta lógica se outros artefactos (snippets, blocks) partilharem a mesma raiz de sandbox; podia apagar dados de outros subsistemas por engano num desinstalar parcial (aqui não acontece porque o plugin desinstala tudo de uma vez, mas é um risco arquitectural a ter em conta numa réplica com desinstalação modular).- A lista de funções
warninginclui explicitamente callbacks dinâmicos (array_map,usort,preg_replace_callback, etc.) precisamente porque são o vector clássico para contornar a listacriticalde chamada directa (ex.:array_map('system', $input)) — um detalhe de segurança sofisticado que vale a pena preservar em qualquer réplica do validador.
5. Fonte
Todos os ficheiros lidos via ssh://server/home/ealmeida/emanuelalmeida.pt/wp-content/plugins/emcp-tools/:
includes/abilities/class-php-snippet-abilities.phpincludes/class-php-snippet-store.phpincludes/class-php-snippet-loader.phpincludes/class-php-snippet-validator.phpincludes/abilities/class-sandbox-cloud-abilities.phpincludes/sandbox/class-sandbox-paths.phpincludes/sandbox/class-snippet-bundle-adapter.phpincludes/sandbox/class-sandbox-bundle.phpincludes/sandbox/class-sandbox-store.phpincludes/sandbox/class-widget-bundle-adapter.phpincludes/sandbox/interface-sandbox-artifact.phpincludes/class-widget-store.phpincludes/admin/class-mcpb-builder.phpincludes/class-widget-loader.php(bónus, consultado para confirmar o runtime companion do Widget Store)includes/admin/views/sandbox/widgets.php(confirmação da UI Pro-gated do Widget Builder)includes/admin/views/sandbox/blocks.php(confirmação de queEMCP_Tools_Block_Storetambém é Pro-only/ausente)includes/admin/views/sandbox/snippets.php(confirmação da UI de aprovação humana dos snippets)includes/abilities/class-ability-registrar.php(grep pontual, linhas ~416-434 e ~549-554, gating exacto dos grupos)includes/admin/class-admin.php(grep pontual: admin-posthandle_download_mcpb+ ajax handlers de toggle/delete de widget/block)includes/abilities/class-custom-code-abilities.php(verificação rápida — confirmar que é feature distinta: Elementor Custom CSS/JS/Code Snippets, não faz parte deste subsistema)- Listagens de directório (via
readem modo directório):includes/abilities/,includes/,includes/admin/,includes/admin/views/sandbox/— usadas para confirmar a ausência declass-widget-builder-abilities.php,class-widget-generator.phpeclass-block-store.phpnesta build Free.