WordPress Filesystem API: the right way to operate with local files

The evolution of WordPress from a blogging platform into a fully-fledged CMS, simultaneously turns it into a solid framework for developers to build outstanding projects and applications upon.

The WordPress core, powers not only the users’ publishing engine but also provides developers with a robust set of classes, APIs and helpers, designed to address a wide range of needs.

One of hidden gems of WordPress that allows developers to perform operations with the local file system in a secure and robust way is the WordPress Filesystem API. It abstracts file manipulation functionality into a set of commonly requested methods so they can be used securely in different hosting environments.

The scope of the problem

There could be several reasons for wanting to write local files in code:

  • Logging of events or operations performed
  • Data exchange with non-WordPress powered systems
  • Backup

Regardless of the motivations, writing local files from PHP code can be a risky operation. At least two very important pitfalls should be taken into account when implementing this for a WordPress theme, plugin or custom install:

  1. Security. There is a risk of incorrect file ownership when writing local files with code (by the webserver). This problem arises in poorly configured shared hosting environments and could lead to the loss of control over files.
  2. Compatibility. Due to the variety of hosting companies out there, the particular user’s server configuration is usually unknown to the developer. Thus, the developer cannot be sure that permissions required for a writing operation are achievable by the user of the plugin or theme.

If a WordPress plugin or theme that needs to write local files is intended for public release, the developer should constantly bear these issues in mind. The good news is that WordPress itself already has a tool to address these problems: the Filesystem API.

Introduction to the WordPress Filesystem API

The Filesystem API was added to WordPress in version 2.6 to enable WordPress’ own update feature. It abstracts the functionality needed to perform read/write operations securely and on a variety of host types. It consists of a set of classes and allows you to choose automatically the proper way of connecting to the local file system, depending on individual host setup.

The logic behind the API is quite simple; it tries to write local files directly and in the case of incorrect file ownership it switches to another FTP-based method. Depending on the available PHP libraries, it finds an appropriate way to setup an FTP connection (via extension sockets, or over-SSH). Generally, the following steps are required to work with local files:

Step 1. Detect what connection method is available

WordPress uses the get_filesystem_method to detect the availability of the following methods (from highest priority to lowest) Direct, SSH2, FTP PHP Extension, FTP Sockets.

Step 2. Obtain credentials required for the detected method

If the detected transport needs credentials from a user, WordPress uses the request_filesystem_credentials function to display a request form. The function has a number of parameters allowing it to preserve data between form submissions, ask for credentials several times if the connection failed, and target to a particular directory inside the WordPress installation:

request_filesystem_credentials($form_post, $type, $error, $context, $extra_fields);

By supplying an empty $type parameter to the function we could force it to perform detection of the available connection methods, so it would call the get_filesystem_method for us. At the same time we can force the function to use any particular connection type by specifying it using the $type argument.

When the connection data required by the chosen method isn’t provided, the function prints the form to request it:

Conneciton information

After the first request WordPress stores the FTP hostname and username in the database for future use, but it does not store the password. Alternatively, FTP credentials could be specified in the wp-config.php file by using following constants:

  • FTP_HOST – the hostname of the server to connect to
  • FTP_USER – the username to connect with
  • FTP_PASS – the password to connect with
  • FTP_PUBKEY – the path to the Public Key to use for SSH2 connection
  • FTP_PRIKEY – the path to the Private Key to use for SSH2 connection

When this data is stored in the wp-config.php file the credentials request form does not appear, but the security drawbacks are significant and safety procedures should be triple-checked with the highest attention possible should be paid to the security of this file.

Step 3. Initialize the WordPress Filesystem class and connect to the file system

The heart of the WordPress Filesystem API is the WP_Filesystem function. It loads and initializes the appropriate transportation class, stores an obtained instance in the global $wp_filesystem object for further usage, and tries to connect to the filesystem with the provided credentials:

WP_Filesystem($args, $context);

Step 4. Use the WordPress Filesystem methods to perform read/write operations

A properly initialized $wp_filesystem object has a set of methods to communicate with the local file system that could be used without any further anxiety about connection type. In particular, there are following commonly used methods:

  • get_contents – reads the file into a string
  • put_contents – writes a string to a file
  • mkdir – creates a directory
  • mdir – removes a directory
  • wp_content_dir – returns the path on the local file system to the wp-content folder
  • wp_plugins_dir – returns the path on the local file system to the plugins folder
  • wp_themes_dir – returns the path on the local file system to the themes folder

Putting it all together, let’s come up with an example that performs the above mentioned steps in a simple situation — we will write some text submitted in a textarea into a plain .txt file.

Note that this example is for demonstration purposes, in a real-world situation you wouldn’t store simple text data in a .txt file, it would be a far more robust solution to store it in the database instead.

The WordPress Filesystem API in action

Let’s wrap our code in a separate plugin, that will be allocated its own filesystem-demo folder. That provides us with target folder to store the .txt file and check writing permissions.

First of all, let’s create the demo page to display our form under the Tools menu:

/**
 * Create Demo page (under Tools menu)
 *
 **/
add_action('admin_menu', 'filesystem_demo_page');

function filesystem_demo_page() {

 add_submenu_page( 'tools.php', 'Filesystem API Demo page', 'Filesystem Demo', 'upload_files', 'filesystem_demo', 'filesystem_demo_screen' );
}

function filesystem_demo_screen() {

$form_url = "tools.php?page=filesystem_demo";
$output = $error = '';

/**
 * write submitted text into file (if any)
 * or read the text from file - if there is no submission
 **/
if(isset($_POST['demotext'])){//new submission

 if(false === ($output = filesystem_demo_text_write($form_url))){
 return; //we are displaying credentials form - no need for further processing

 } elseif(is_wp_error($output)){
 $error = $output->get_error_message();
 $output = '';
 }

} else {//read from file

 if(false === ($output = filesystem_demo_text_read($form_url))){
 return; //we are displaying credentials form no need for further processing

 } elseif(is_wp_error($output)) {
 $error = $output->get_error_message();
 $output = '';
 }
}

$output = esc_textarea($output); //escaping for printing

?>
<div class="wrap">
<div id="icon-tools" class="icon32"></div>
<h2>Filesystem API Demo page</h2>
</div>

<!--?php if(!empty($error)): ?-->
<div class="error below-h2"><!--?php echo $error;?--></div>

<!--?php endif; ?-->
<form method="post">
<!--?php wp_nonce_field('filesystem_demo_screen'); ?-->
<fieldset class="form-table">
 <label for="demotext">
 <textarea id="demotext" class="large-text" rows="8" name="demotext"><?php echo $output;?></textarea>
</label></fieldset>

<!--?php submit_button('Submit', 'primary', 'demotext_submit', true);?-->
</form>

When displaying our page (filesystem_demo_screen) we check for the availability of text submission. If it exists we try to write it in a test.txt file, otherwise, we try to find such a file in plugin folder and read its content to be included in textarea. Finally we print a basic form to input text. For the sake of readability these writing and reading operations were separated into their own functions.

Filesystem API demo

To avoid duplication of the same initialization steps the shared helper has been created. It calls request_filesystem_credentials first to detect the available connection method and obtain credentials. If that was successful it then calls WP_Filesystem to initiate $wp_filesystem with given data.

/**
 * Initialize Filesystem object
 *
 * @param str $form_url - URL of the page to display request form
 * @param str $method - connection method
 * @param str $context - destination folder
 * @param array $fields - fileds of $_POST array that should be preserved between screens
 * @return bool/str - false on failure, stored text on success
 **/
function filesystem_init($form_url, $method, $context, $fields = null) {
 global $wp_filesystem;

 /* first attempt to get credentials */
 if (false === ($creds = request_filesystem_credentials($form_url, $method, false, $context, $fields))) {

 /**
 * if we comes here - we don't have credentials
 * so the request for them is displaying
 * no need for further processing
 **/
 return false;
 }

 /* now we got some credentials - try to use them*/
 if (!WP_Filesystem($creds)) {

 /* incorrect connection data - ask for credentials again, now with error message */
 request_filesystem_credentials($form_url, $method, true, $context);
 return false;
 }

 return true; //filesystem object successfully initiated
}

Writing to file code looks like this:

/**
 * Perform writing into file
 *
 * @param str $form_url - URL of the page to display request form
 * @return bool/str - false on failure, stored text on success
 **/
function filesystem_demo_text_write($form_url){
 global $wp_filesystem;

 check_admin_referer('filesystem_demo_screen');

 $demotext = sanitize_text_field($_POST['demotext']); //sanitize the input
 $form_fields = array('demotext'); //fields that should be preserved across screens
 $method = ''; //leave this empty to perform test for 'direct' writing
 $context = WP_PLUGIN_DIR . '/filesystem-demo'; //target folder

 $form_url = wp_nonce_url($form_url, 'filesystem_demo_screen'); //page url with nonce value

 if(!filesystem_init($form_url, $method, $context, $form_fields))
 return false; //stop further processign when request form is displaying

 /*
 * now $wp_filesystem could be used
 * get correct target file first
 **/
 $target_dir = $wp_filesystem->find_folder($context);
 $target_file = trailingslashit($target_dir).'test.txt';

 /* write into file */
 if(!$wp_filesystem->put_contents($target_file, $demotext, FS_CHMOD_FILE))
 return new WP_Error('writing_error', 'Error when writing file'); //return error object

 return $demotext;
}

In this part we defined some necessary parameters:

  • $demotext — submitted text to write
  • $form_fields — item in the $_POST array that stores our text and should be preserved
  • $method — transportation method, we leave it blank to detect automatically
  • $context — target folder (the plugin’s one)

After that we initiated the global $wp_filesystem object using the helper function I described earlier. In case of success we detect the correct path to the target folder and write the submitted text into it using put_contents method of the $wp_filesystem object.

The code for reading from the file looks like this:

/**
 * Read text from file
 *
 * @param str $form_url - URL of the page where request form will be displayed
 * @return bool/str - false on failure, stored text on success
 **/
function filesystem_demo_text_read($form_url){
 global $wp_filesystem;

 $demotext = '';

 $form_url = wp_nonce_url($form_url, 'filesystem_demo_screen');
 $method = ''; //leave this empty to perform test for 'direct' writing
 $context = WP_PLUGIN_DIR . '/filesystem-demo'; //target folder 

 if(!filesystem_init($form_url, $method, $context))
 return false; //stop further processing when request forms displaying

 /*
 * now $wp_filesystem could be used
 * get correct target file first
 **/
 $target_dir = $wp_filesystem->find_folder($context);
 $target_file = trailingslashit($target_dir).'test.txt';

 /* read the file */
 if($wp_filesystem->exists($target_file)){ //check for existence

 $demotext = $wp_filesystem->get_contents($target_file);
 if(!$demotext)
 return new WP_Error('reading_error', 'Error when reading file'); //return error object 

 } 

 return $demotext;
}

This function works in the same way as previously described, but it uses get_contents to read from target file.

Conclusion

When working with local files, a WordPress themes or plugins developer will come into contact with security and compatibility issues, putting enormous stress on the team and adding long hours to the project life-cycle. By relying on the Filesystem API these problems can be side-stepped in an efficient manner. So the next time you find yourself writing fwrite into your plugin’s code, consider this alternative the healthier option.

You can download a demo of this code here, and adapt it to your needs.

Anna Ladoshkina

Anna Ladoshkina

Anna Ladoshkina is a freelance web designer and developer who likes to build pretty things with WordPress and write about it. Connect with Anna on Twitter (@foralien) or on her website, www.foralien.com.

Join to our thriving community of like-minded creatives!