Filesystem API

This article is part of the WordPress guide. Read the introduction.

The Filesystem API is a family of WordPress functions and methods used to interact with the server’s filesystem. It includes methods such as get_contents(), put_contents(), mkdir(), and more. The first question you might ask is – why? Why does WordPress have a proprietary filesystem API when functions like file_put_contents() exist in PHP? The short answer is – file ownership.

Let’s assume that you uploaded your WordPress files with user deployer:deployer. All of your core files are owned by this user, but your web server runs as www-data:www-data. If you tried to create a file using PHP functions, the file would be owned by www-data:www-data. Suddenly, your WordPress files have mixed owners.

This creates a few problems. The deployer user might not be able to edit files owned by www-data (and vice versa). It can also introduce security questions in certain badly configured shared hosting environments, where multiple websites are run by the same web server, and when you don’t want code run by your web server to be able to modify crucial files (like wp-config.php or .htaccess). The Filesystem API solves these problems by switching to FTP when file ownership mismatch is detected (more details soon).

When To Use The Filesystem API

Let me start by saying that the likelihood you will ever need to use this API is incredibly low, even if your plugin needs to create files. The Filesystem API was originally added in WordPress 2.6 to support WordPress’s own automatic update feature. To this day, it is used whenever you update or install a plugin or theme.

When you’re unsure if you should use this API, ask yourself this question – “Is the file I’m creating important enough to be version-controlled?”. If the answer is no, it’s likely that you can use built-in PHP functions directly. The essence of the question is that not every file needs careful ownership management.

Examples of use cases where the Filesystem API is not necessary:

  • Creating files in a directory that is inherently supposed to contain “content”, e.g., /wp-content/uploads.
  • Creating disposable files, e.g., logs, cached pages, temporary files, exports, backups, etc.

Examples of use cases where the Filesystem API is necessary:

  • Creating/modifying core WordPress files, e.g., in /wp-admin/ or /wp-includes/ (don’t).
  • Creating/modifying files that are part of the “application code”, e.g., plugin files with a proprietary plugin update system.
  • Creating/modifying important configuration files, e.g., wp-config.php, .htaccess, etc.

The underlying distinction is between critical application files and runtime data/user content. Application files have different security expectations. It is usually not desirable for your web server to be able to tinker with core WordPress files (even though that’s the default if you use the one-click WordPress installer). Similarly, mixed owners within those files might prove problematic if a deployment script is used and one of the version-controlled files has a different owner on the server. In such a case, the script would fail.

In contrast, files like debug.log or temporary full-page caches are supposed to be created and modified by the web server. They do not contain critical site code and are usually not included in version control. Having those files not be writable by the web server would mean that for every write operation, WordPress would need to connect to the server through FTP as the user that owns these files. That’s why WordPress itself doesn’t use the Filesystem API for uploading files to /wp-content/uploads.

How It Works

You already know the most important thing you can – why it exists and when to use it. Trust me, I wasn’t able to find a single good explanation of that anywhere. It’s time you see how it works.

The core of the API is located in the /wp-admin/includes/file.php file. This file is inside the wp-admin directory because the API is supposed to only be used in the admin panel. It is not loaded on the frontend by default.

Four connection types are supported and chosen in the following order of preference:

  1. Direct (using built-in PHP functions).
  2. SSH2.
  3. FTPS.
  4. FTP.

The beauty of this API is that it provides a unified interface for all these different connection types. The API follows the Singleton pattern, where the global $wp_filesystem is an initialized object for the selected connection type. All connection type classes extend the WP_Filesystem_Base class. This class includes methods like:

  • chmod()
  • chown()
  • copy()
  • get_contents()
  • mkdir()
  • put_contents()
  • and many more…

To use this API, you can call methods like: $wp_filesystem->put_contents( $file, $contents ). That being said, the $wp_filesystem object is not initialized on every admin request. It is null by default. To initialize it, you have to manually call request_filesystem_credentials(). This function is the most confusing, yet the most important part of the Filesystem API.

I’m going to start with a short summary of how it works, and then we’ll get to the step-by-step initialization process. request_filesystem_credentials() returns an array of credentials you pass to WP_Filesystem() to initialize $wp_filesystem. It checks if either direct or SSH2 can be used. If they can’t, it tries to find FTP credentials defined in wp-config.php. If there are none, it outputs an HTML form asking the user to input the credentials.

Below is a full step by step process of initializing $wp_filesystem (the most complicated part of this API). I will show you a concrete code example shortly after.

  1. The user triggers some action that requires the Filesystem API (e.g., clicks the “update” button).
  2. The code hooked to run on this action (e.g, on this POST request) starts off by calling request_filesystem_credentials() and storing its return value in $creds.
  3. request_filesystem_credentials() does the following:
    1. Try to find the credentials in $_POST.
    2. Check if the direct method can be used by creating a temporary file using fopen() and comparing its owner to the owner of /wp-admin/includes/file.php.
    3. If direct can’t be used, check if constants like FTP_HOST, FTP_USER, and FTP_PASS are defined in wp-config.php.
    4. If constants aren’t defined, render an HTML form asking the user to choose a connection type (SSH/SFTP/FTP if appropriate PHP extensions are loaded on the server) and input credentials.
    5. If the form is rendered, return false. If credentials are obtained, return an array containing them.
  4. If $creds is false, it means that the form has been rendered. We call wp_die().
  5. If $creds is not false, we pass it as an argument to WP_Filesystem(). If this function returns true, it means $wp_filesystem has been successfully initialized. If it returns false, it means the credentials were incorrect.

There are a few interesting things here. First of all, you can see that $wp_filesystem should usually be initialized on interactive user action, e.g., submitting a form, clicking a button, etc. That’s because there is a possibility that the user will have to fill out a form.

The request_filesystem_credentials() function starts by looking for the credentials in $_POST. It is meant to find them after the user has submitted the form it previously rendered. It will only ever find them on the “second execution” of request_filesystem_credentials(). This means that you need to ensure the original code that you ran after the user has clicked the update button, also runs after they submit the credentials form.

Thankfully, request_filesystem_credentials() allows you to specify the URL the form will be POSTed to, along with additional hidden input fields, which you can use to preserve the data POSTed by the user in their original request (when they clicked “update”). Here’s a simple visualization if you don’t understand:

  1. The user clicks “update”. A POST request is sent to the server with “plugin_id” = 1. Your code only executes if “plugin_id” is present in $_POST and if the POST is sent to /wp-admin/update, so it’s going to execute.
  2. Your code calls request_filesystem_credentials(). It specifies /wp-admin/update as the path the form will POST to (its action attribute). It also specifies “plugin_id” = 1 in the extra fields parameter, which will render that as a hidden input field.
  3. The user fills out the credentials form and submits. It POSTs to /wp-admin/update, and the “plugin_id” field exists, so your code runs again. request_filesystem_credentials() is called (again), but this time, it finds the credentials in $_POST, so it doesn’t have to render the form.
  4. $wp_filesystem is initialized and you can use it in the rest of your code.

Code Example

Here’s a code example roughly following what I just described (minus the URL path check and POSTing to /wp-admin/):

PHP
function fsapi() {
    if ( ! isset( $_POST['pgn_plugin_id'] ) ) {
        return;
    }

    global $wp_filesystem;
    $creds = request_filesystem_credentials(
        admin_url(),
        '',
        false,
        '',
        array( 'pgn_plugin_id' => $_POST['pgn_plugin_id'] ) // This will become a hidden form field.
    );

    // Direct couldn't be used and credentials constants weren't found.
    if ( ! $creds ) {
        wp_die();
    }

    // Credentials were incorrect so we're setting the "error" argument to true.
    if ( ! WP_Filesystem( $creds ) ) {
        request_filesystem_credentials(
            admin_url(),
            '',
            true,
            '',
            array( 'pgn_plugin_id' => $_POST['pgn_plugin_id'] ) // Still preserving original data.
        );
        wp_die();
    }

    // $wp_filesystem is initialized, we can proceed.
    $wp_filesystem->put_contents( plugin_dir_path( __FILE__ ) . 'test.txt', 'abc', FS_CHMOD_FILE );
}
add_action( 'admin_init', 'fsapi' );

You can see that we’re hooking this function to admin_init, but we’re only actually running the code if “pgn_plugin_id” is present in the $_POST. The return value of the first request_filesystem_credentials() is captured to $creds and checked. It’s going to return false at first because my /wp-admin/includes/file.php file has a different owner than www-data and I don’t have constants configured. Here’s what the user (admin) will see after submitting the form with “pgn_plugin_id”:

This form will be the only thing rendered on the entire page (because we called wp_die()). You can see that there are only two connection types – FTP and FTPS. There’s no SSH because I don’t have the ssh2 PHP extension enabled on my server. Look back up at the code. When $creds is not false but WP_Filesystem() fails, it means that the credentials were wrong, so we request them again, this time passing the error flag. Here’s what the form will look like after submitting bad credentials:

You can see “Error: Could not connect to the server. Please verify the settings are correct.” at the top of the form. If $wp_filesystem gets initialized successfully, we write “abc” to the file test.txt inside the current plugin’s directory.

Using The Filesystem API In Non-Interactive Environments

Up to now, we’ve only talked about using the Filesystem API in an environment where it’s the user that prompts the action. They submit a form or click a button. This is an interactive environment, because we can render the form and expect the user to be able to fill it out. But what if that’s not the case? What if, for example, we had to use the Filesystem API in a cron job?

In such a case, you would not be able to depend on the user input. The only way you can use this API like that is by forcing the user to define their credentials as constants in wp-config.php. You would also have to suppress the rendering of the form (like with output buffering) and throw a definitive error if the user has failed to define the constants. Just bear in mind that it’s more complicated and non-standard, but not impossible.