DesenvolvedorWordPress WhatsApp

WP_Filesystem: manipular arquivos no WordPress com segurança

Ler, escrever e apagar arquivos parece trivial em PHP — até você descobrir que file_put_contents() direto num plugin quebra em metade das hospedagens. A API WP_Filesystem existe justamente para resolver isso, abstraindo permissões e credenciais FTP/SSH. Veja como usá-la do jeito certo.

Precisa de desenvolvimento sob medida?

Atualizado em 19/06/2026

Quase todo desenvolvedor vindo do PHP "puro" comete o mesmo deslize ao programar para WordPress: usar fopen(), file_put_contents() e unlink() diretamente para mexer em arquivos. O código até funciona na sua máquina e no seu servidor — e falha exatamente no cliente que mais importa, porque a propriedade dos arquivos e as permissões variam de uma hospedagem para outra. A resposta do núcleo do WordPress para esse problema é a WP_Filesystem API, uma camada de abstração que decide sozinha se vai escrever direto no disco, via FTP ou via SSH, mantendo o seu código idêntico nos três casos.

O problema: por que não escrever arquivos "no PHP cru"

Em um servidor bem configurado, o processo do PHP (por exemplo, www-data ou um usuário PHP-FPM dedicado) não é o dono dos arquivos do site. Isso é proposital: separa quem executa código de quem possui os arquivos, reduzindo o estrago de uma invasão. As consequências práticas para quem escreve plugins são três:

A WP_Filesystem contorna tudo isso oferecendo uma abstração de credenciais: quando não dá para escrever direto, ela usa as credenciais de FTP/SSH (que o dono do site de fato possui) para realizar a operação com a identidade correta. Para você, programador, a interface é sempre a mesma.

Inicializando a API corretamente

A função WP_Filesystem() vive em wp-admin/includes/file.php, que não está carregado em todo contexto (front-end, cron e até parte do AJAX não o incluem). Por isso o padrão de inicialização é sempre o mesmo: garanta o arquivo, chame WP_Filesystem() e só então use a global $wp_filesystem.

<?php
/**
 * Garante que a WP_Filesystem esteja pronta e devolve o objeto global.
 * Retorna null se não foi possível inicializar (ex.: faltam credenciais).
 */
function meu_plugin_fs() {
    global $wp_filesystem;

    // 1) Carrega a definição de WP_Filesystem() quando necessário.
    if ( ! function_exists( 'WP_Filesystem' ) ) {
        require_once ABSPATH . 'wp-admin/includes/file.php';
    }

    // 2) Inicializa. Sem credenciais explícitas, tenta o método Direct.
    if ( ! WP_Filesystem() ) {
        return null; // transporte exige FTP/SSH e não temos credenciais
    }

    return $wp_filesystem;
}

Armadilha clássica: acessar $wp_filesystem antes de chamar WP_Filesystem(). A global só passa a existir depois da inicialização bem-sucedida. Antes disso ela é null, e qualquer $wp_filesystem->put_contents(...) dispara um fatal error de chamada a método em null.

Escrevendo um arquivo dentro de uploads

O lugar seguro para gravar é o diretório de uploads, obtido por wp_upload_dir() — é o único caminho que o WordPress garante existir e ser gravável em qualquer instalação. Use FS_CHMOD_FILE em vez de cravar 0644: o administrador pode ter redefinido essa constante no wp-config.php para casar com a hospedagem.

<?php
function meu_plugin_salvar_relatorio( $conteudo ) {
    $fs = meu_plugin_fs();
    if ( ! $fs ) {
        return new WP_Error( 'fs_indisponivel', 'WP_Filesystem não inicializou.' );
    }

    $uploads = wp_upload_dir();
    if ( ! empty( $uploads['error'] ) ) {
        return new WP_Error( 'uploads_erro', $uploads['error'] );
    }

    // Subpasta dedicada ao plugin, criada com a permissão certa.
    $dir = trailingslashit( $uploads['basedir'] ) . 'meu-plugin';
    if ( ! $fs->is_dir( $dir ) ) {
        $fs->mkdir( $dir, FS_CHMOD_DIR );
    }

    $arquivo = trailingslashit( $dir ) . 'relatorio.txt';

    // put_contents devolve bool. FS_CHMOD_FILE define a permissão correta.
    $ok = $fs->put_contents( $arquivo, $conteudo, FS_CHMOD_FILE );

    return $ok ? $arquivo : new WP_Error( 'escrita_falhou', 'Não foi possível gravar.' );
}

Lendo e checando a existência

Para ler, combine exists() (nunca assuma que o arquivo está lá) com get_contents(), que devolve a string do arquivo ou false em caso de erro. Para arquivos linha a linha existe get_contents_array().

<?php
function meu_plugin_ler_relatorio() {
    $fs = meu_plugin_fs();
    if ( ! $fs ) {
        return '';
    }

    $uploads = wp_upload_dir();
    $arquivo = trailingslashit( $uploads['basedir'] ) . 'meu-plugin/relatorio.txt';

    if ( ! $fs->exists( $arquivo ) ) {
        return ''; // ainda não foi gerado
    }

    $conteudo = $fs->get_contents( $arquivo );
    return ( false === $conteudo ) ? '' : $conteudo;
}

Repare que mesmo a leitura passa pela API. Em transporte FTP/SSH, ler com file_get_contents() nativo pode até funcionar (leitura costuma ser permitida), mas misturar as duas abordagens leva a bugs sutis de caminho e de codificação. Mantenha tudo dentro do $wp_filesystem para um comportamento previsível.

O fluxo de credenciais em uma tela de admin

Quando o transporte não é Direct, o WordPress precisa pedir as credenciais de FTP/SSH ao usuário. A função request_filesystem_credentials() faz esse trabalho pesado: ela tenta usar credenciais já conhecidas e, se faltar algo, imprime o formulário e retorna false. O fluxo correto é sempre o mesmo — e ele só funciona em uma página de admin, porque precisa de uma tela para exibir o formulário e de uma URL para onde reenviar o POST.

<?php
add_action( 'admin_post_meu_plugin_gerar', 'meu_plugin_handler_gerar' );

function meu_plugin_handler_gerar() {
    // Sempre cheque capacidade e nonce antes de qualquer escrita.
    if ( ! current_user_can( 'manage_options' ) ) {
        wp_die( 'Sem permissão.' );
    }
    check_admin_referer( 'meu_plugin_gerar' );

    require_once ABSPATH . 'wp-admin/includes/file.php';

    // URL para onde o formulário de credenciais deve reenviar o POST.
    $url = wp_nonce_url(
        admin_url( 'admin-post.php?action=meu_plugin_gerar' ),
        'meu_plugin_gerar'
    );

    // 1ª passada: descobre se precisamos de credenciais.
    $creds = request_filesystem_credentials( $url, '', false, false, null );
    if ( false === $creds ) {
        // O formulário já foi impresso por request_filesystem_credentials(). Pare aqui.
        return;
    }

    // 2ª passada: tenta conectar com as credenciais informadas.
    if ( ! WP_Filesystem( $creds ) ) {
        // Credenciais erradas: imprime o formulário de novo, agora com erro.
        request_filesystem_credentials( $url, '', true, false, null );
        return;
    }

    // A partir daqui $wp_filesystem está pronto para escrever.
    meu_plugin_salvar_relatorio( 'Gerado em ' . current_time( 'mysql' ) );

    wp_safe_redirect( admin_url( 'admin.php?page=meu-plugin&done=1' ) );
    exit;
}

O detalhe que mais confunde: request_filesystem_credentials() é chamado duas vezes. Na primeira, com o quarto parâmetro $error = false, ela apenas verifica e, se necessário, mostra o formulário. Na segunda (após uma tentativa de conexão que falhou), você passa $error = true para reexibir o formulário sinalizando o erro. Esse padrão de "duas passadas" é exatamente o que o próprio instalador de plugins do WordPress usa.

Os métodos do $wp_filesystem e seus equivalentes "crus"

A classe base WP_Filesystem_Base define a interface comum a todos os transportes. Sempre que possível, prefira o método da API ao equivalente nativo — a coluna da direita só funciona em modo Direct.

WP_FilesystemPHP "cru" equivalentePor que preferir a API
$wp_filesystem->exists( $f )file_exists()Resolve o caminho conforme o transporte (raiz FTP ≠ caminho absoluto).
$wp_filesystem->get_contents( $f )file_get_contents()Leitura consistente mesmo via FTP/SSH.
$wp_filesystem->put_contents( $f, $d, FS_CHMOD_FILE )file_put_contents() + chmod()Dono e permissões corretos; respeita as constantes do site.
$wp_filesystem->mkdir( $d, FS_CHMOD_DIR )mkdir()Cria a pasta com a identidade certa e a permissão padrão do WP.
$wp_filesystem->delete( $f )unlink() / rmdir()Apaga arquivo ou pasta (com $recursive) via qualquer transporte.
$wp_filesystem->move( $a, $b )rename()Move/renomeia respeitando permissões; tem flag de sobrescrita.
$wp_filesystem->copy( $a, $b, false, FS_CHMOD_FILE )copy()Copia já aplicando a permissão de destino.

Métodos de transporte: Direct, FTP e SSH2

Por baixo, a fábrica do WordPress escolhe uma implementação concreta a partir de get_filesystem_method():

Nunca assuma o método Direct. É o erro de arquitetura mais comum: o plugin funciona em todo ambiente do desenvolvedor (onde o PHP é dono dos arquivos) e falha no cliente. Sempre trate o caso em que WP_Filesystem() retorna false ou em que request_filesystem_credentials() precisa do formulário.

Validando caminhos: evite path traversal

Se qualquer parte do caminho vier de dados do usuário (nome de arquivo, ID, slug), um atacante pode injetar ../ e tentar escapar do diretório de uploads. A defesa é montar o caminho a partir de uma base confiável, normalizar e conferir o prefixo:

<?php
function meu_plugin_caminho_seguro( $nome ) {
    $uploads = wp_upload_dir();
    $base    = wp_normalize_path( trailingslashit( $uploads['basedir'] ) . 'meu-plugin' );

    // Remove diretórios do nome enviado e caracteres perigosos.
    $nome  = sanitize_file_name( wp_basename( $nome ) );
    $alvo  = wp_normalize_path( trailingslashit( $base ) . $nome );

    // Garante que o caminho final ainda está DENTRO da base.
    if ( 0 !== strpos( $alvo, trailingslashit( $base ) ) ) {
        return new WP_Error( 'path_invalido', 'Caminho fora do diretório permitido.' );
    }

    return $alvo;
}

A dupla wp_basename() + sanitize_file_name() remove componentes de diretório e neutraliza caracteres problemáticos; a checagem de prefixo com strpos() === 0 fecha a porta para ../. Esse cuidado se conecta diretamente com os princípios de segurança no código WordPress: validar tudo que entra.

Boas práticas

Manipulação de arquivos costuma aparecer quando você está construindo um plugin do zero — gerar um CSV de exportação, salvar um cache em disco, escrever um arquivo de configuração. Tratar isso pela WP_Filesystem desde o início é o que separa um plugin que "funciona no meu servidor" de um que funciona em qualquer hospedagem do cliente.

Precisa de um plugin que manipule arquivos com segurança em qualquer hospedagem?

Desenvolvo plugins usando a WP_Filesystem e os padrões de segurança do WordPress, para o código não quebrar em FTP/SSH. Me chame no WhatsApp.

WhatsApp: (43) 99932-9697

ou e-mail: [email protected]

Perguntas frequentes

Por que não usar fopen() ou file_put_contents() direto num plugin?

Porque o usuário que roda o PHP nem sempre é o dono dos arquivos do site. Em muitas hospedagens compartilhadas o WordPress só consegue escrever via FTP/SSH, e arquivos criados pelo PHP nativo podem ficar com dono e permissões errados — quebrando atualizações e, pior, criando arquivos graváveis pelo grupo (porta de entrada para invasão). A WP_Filesystem abstrai isso: o mesmo código funciona em modo Direct, FTP ou SSH2 sem que você mude uma linha.

Preciso sempre chamar request_filesystem_credentials()?

Só quando o método de transporte não for Direct. Em servidores onde o PHP é dono dos arquivos, WP_Filesystem() conecta sozinho e você nunca vê o formulário. Quando o transporte exige FTP/SSH, o WordPress precisa das credenciais; o fluxo correto é chamar request_filesystem_credentials() primeiro, exibir o formulário se ele retornar false e só então inicializar. Pular esse passo faz WP_Filesystem() falhar silenciosamente em parte das hospedagens.

Onde devo gravar arquivos com segurança?

Dentro do diretório de uploads, obtido com wp_upload_dir() — é o único local que o WordPress garante existir e ser gravável. Nunca escreva em caminho fixo como /var/www/... nem monte caminhos com dados do usuário sem validar. Sempre normalize o caminho final (com wp_normalize_path e checagem de prefixo) para evitar path traversal com ../.

Qual a diferença entre FS_CHMOD_FILE e FS_CHMOD_DIR?

São as constantes que o WordPress define com as permissões corretas para o ambiente: FS_CHMOD_FILE para arquivos (tipicamente 0644) e FS_CHMOD_DIR para diretórios (tipicamente 0755). Passe-as em put_contents() e mkdir() em vez de cravar números — o admin pode tê-las redefinido no wp-config.php para casar com a hospedagem.

Posso usar $wp_filesystem em qualquer lugar do código?

Não antes de inicializar. A variável global $wp_filesystem só existe depois de uma chamada bem-sucedida a WP_Filesystem(), que por sua vez depende de wp-admin/includes/file.php estar carregado. Em contexto de front-end ou cron esse arquivo não é incluído automaticamente; faça o require_once antes. Usar $wp_filesystem sem isso gera um fatal de "chamada a método em null".

A WP_Filesystem funciona em requisições AJAX e WP-Cron?

Funciona, mas com ressalvas. Em AJAX/cron não há tela para exibir o formulário de credenciais, então a operação só é viável se o transporte for Direct ou se as credenciais já estiverem disponíveis (definidas em wp-config.php ou guardadas). Por isso operações de escrita pesadas costumam ser disparadas a partir de uma tela de admin, onde request_filesystem_credentials() pode interagir com o usuário.

Referências oficiais

  1. WordPress.org — Filesystem API (Common APIs)
  2. WordPress.org — Classe WP_Filesystem_Base (Code Reference)
  3. WordPress.org — request_filesystem_credentials() (Code Reference)
Tem alguma dúvida? Ficarei feliz em ajudar. Clique Aqui