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

Default avatar.
August 21, 2012

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 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.

Read Next

AI Changes Everything and Nothing

The marketing frenzy surrounding the recent flood of AI-powered apps and services has caused some observers to question…

15 Best New Fonts, March 2023

Fonts are one of the most critical tools in any designer’s toolbox. With clever use, you can transform a design from hu…

20 Best New Websites, March 2023

We have another exciting collection of the best new sites on the web for you. In this month’s episode, there are severa…

Exciting New Tools for Designers, March 2023

We have invoicing apps and scheduling tools. Some resources will save you the trouble of hiring a designer or developer…

Free Download: Budget Planner UI Kit

Designing an onboarding process can be tricky; there are so many different options, and if you get it wrong, you could …

3 Essential Design Trends, February 2023

There’s a common theme in this month’s collection of website design trends – typography. All three of these trends show…

Free Download: Education Icons

Icons are essential for successful web design. They provide an eye-catching, unobtrusive way to communicate important i…

15 Best New Fonts, February 2023

The fonts you embed in your website transform the design and can mean the difference between an extraordinary brand exp…

Unlocking the Power of Design to Help Users Make Smart Decisions

Users are faced with decision-making on websites every day. The decision-making process can be far more complex than it…

20 Best New Websites, February 2023

The quality of websites in 2023 has moved up a gear, with designers cherry-picking trends as tools, embracing new ideas…

AI’s Impact on the Web Is Growing

Despite the massive strides tech has taken in the last few years, we rarely see a week as tumultuous as this. When your…

Exciting New Tools for Designers, February 2023

No matter what you’re working on, you can guarantee that there’s a cool app, resource, or service that will help you do…