WordPress Filesystem API: the right way to operate with local files
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
- 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.
- 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.
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:
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 toFTP_USER
- the username to connect withFTP_PASS
- the password to connect withFTP_PUBKEY
- the path to the Public Key to use for SSH2 connectionFTP_PRIKEY
- the path to the Private Key to use for SSH2 connection
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
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. 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)
/** * 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
3 Essential Design Trends, November 2024
Touchable texture, distinct grids, and two-column designs are some of the most trending website design elements of…
20 Best New Websites, October 2024
Something we’re seeing more and more of is the ‘customizable’ site. Most often, this means a button to swap between…
Exciting New Tools for Designers, October 2024
We’ve got goodies for designers, developers, SEO-ers, content managers, and those of you who wear multiple hats. And,…
15 Best New Fonts, September 2024
Welcome to our roundup of the best new fonts we’ve found on the web in the previous four weeks. In this month’s edition…
By Simon Sterne
3 Essential Design Trends, October 2024
This article is brought to you by Constantino, a renowned company offering premium and affordable website design
You…
A Beginner’s Guide to Using BlueSky for Business Success
In today’s fast-paced digital world, businesses are always on the lookout for new ways to connect with their audience.…
By Louise North
The Importance of Title Tags: Tips and Tricks to Optimize for SEO
When it comes to on-page SEO, there’s one element that plays a pivotal role in both search engine rankings and user…
By Simon Sterne
20 Best New Websites, September 2024
We have a mixed bag for you with both minimalist and maximalist designs, and single pagers alongside much bigger, but…
Exciting New Tools for Designers, September 2024
This time around we are aiming to simplify life, with some light and fast analytics, an all-in-one productivity…
3 Essential Design Trends, September 2024
September's web design trends have a fun, fall feeling ... and we love it. See what's trending in website design this…
Crafting Personalized Experiences with AI
Picture this: You open Netflix, and it’s like the platform just knows what you’re in the mood for. Or maybe you’re…
By Simon Sterne
15 Best New Fonts, August 2024
Welcome to August’s roundup of the best fonts we’ve found over the last few weeks. 2024’s trend for flowing curves and…
By Ben Moss