I’ve spent a lot of time lately dealing with a little known, under-documented but extremely useful class in WordPress called WP_Filesystem. WP_Filesystem is used throughout WordPress to handle core/plugin/themes updates and various tasks that require writing files to your webserver.

The WP_Filesystem class is a base class that has many extensions depending on what ‘method’ it can use to do the work it needs to do (install a plugin, upgrade core, move a file, etc.). There are many methods that WP_Filesystem can utilize based on your server setup and permissions:

  • Direct
  • FTP
  • FTP Sockets
  • SSH2

The beauty of how the WP_Filesystem class works is that it chooses the best option based on the user’s setup, AND it makes sure that file ownership is correct before it performs any actions. It’s a win-win for everyone.

WP_Filesystem Overview

WordPress provides a nifty function called request_filesystem_credentials() that does all the hard work for you in setting up WP_Filesystem.

The request_filesystem_credentials() function accepts 5 parameters:

  • $form_post
  • $method
  • $error
  • $context
  • $extra_fields
  • .

$form_post is the URL in which the resulting form should be posted to. Security is always an issue, so you should be using wp_nonce_url() to build this URL, where the nonce field matches the nonce in the submitted form field. You can easily pass along extra query args here as well using add_query_arg().

$method is the method in which you want WP_Filesystem to use. Because WP_Filesystem automatically determines and populates the best method for use, there’s really no need to specify a particular method here unless you are doing it for testing purposes. A good use-case would be to force a particular method and ensure that WP_Filesystem will write files to your webserver correctly if it can’t verify the ownership of files.

$error is a boolean to specify whether you want to output an error message if WP_Filesystem fails to connect. It is false by default, but can be helpful to turn on for testing and debugging.

$context is the directory in which you want to test WP_Filesystem so that it can verify ownership of files. By default, it will attempt to write a temporary file to the wp-content directory (specified by the constant WP_CONTENT_DIR). This field can be useful if you want to test the particular directory in which you are about to write files.

$extra_fields are extra $_POST fields from the previous form that should be included in the resulting post form. The $_POST fields must be strings (arrays are not currently accepted – see this ticket for more info).

Now that we have dissected request_filesystem_credentials(), let’s put it into action.

Initializing WP_Filesystem

For your convenience, I’ve created and supplied a small plugin to demonstrate how WP_Filesystem works. It writes a simple text file to your wp-content directory.

Let’s dissect the code so we can see what is actually going on. The plugin creates a new class, TGM_WP_Filesystem, that will create the object that gets our plugin rolling. I’m not going to include every single piece of code from the plugin in this tutorial, so make sure to follow along in the plugin as I run particular bits and pieces of it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Constructor. Hooks all interactions into correct areas to start
* the class.
*
* @since 1.0.0
*/
public function __construct() {
 
	/** Load the plugin in the plugins_loaded hook */
	add_action( 'plugins_loaded', array( $this, 'init' ) );
 
}
 
/**
* Loads the plugin hooks and filters.
*
* @since 1.0.0
*/
public function init() {
 
	/** Create our options page */
	add_action( 'admin_menu', array( $this, 'options_page' ) );
 
}

The first method in the above code, __construct(), initializes the class and loads everything into the plugins_loaded hook. Not that we have any plugin dependencies for this particular tutorial, but it is a good practice to do this in order to minimize conflicts with other plugins.

The second method adds the hook to create our options page, which is listed below:

1
2
3
4
5
6
7
8
9
10
11
/**
* Registers the options page with WordPress.
*
* @since 1.0.0
*/
public function options_page() {
 
	/** Register our options page and store the pagehook (if we needed) */
	$this->pagehook = add_options_page( __( 'TGM WP_Filesystem Options' ), __( 'TGM Options' ), 'manage_options', 'tgm-wp-filesystem', array( $this, 'options_page_cb' ) );
 
}

We are using add_options_page to register our options page for the plugin. $this->pagehook is the property that will hold the unique options page slug that is returned by default by the add_options_page function.

Now let’s take a look at the callback method, options_page_cb(), that will output our form for writing our simple test file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* Callback function for the options page.
*
* @since 1.0.0
*/
public function options_page_cb() {
 
	/** If we are performing a WP_Filesystem action, return early - we don't need to display the extra information */
	if ( $this->write_test_file() )
	return;
 
	/** If our test file has been written and/or it exists, notify the user */
	if ( $this->test_file )
	echo '<div id="message" class="updated"><p>' . __( 'Your test file has been written successfully!' ) . '</p></div>';
 
	/** We aren't performing an action, so output a form */
	echo '<div class="wrap">';
		screen_icon( 'options-general' );
		echo '<h2>' . esc_html( get_admin_page_title() ) . '</h2>';
		echo '<p>' . __( 'The button below will be used to write a test file to your server. When the test file has been written successfully to your server, a notice will appear above this text. If more credentials are needed to verify file ownership, a form will be output asking for those credentials.' ) . '</p>';
		echo '<form action="" method="post">';
			/** Make sure we keep security first so we don't accidentally set this form off */
			wp_nonce_field( 'tgm-wp-filesystem-nonce' );
			submit_button( __( 'Write My Test File!' ), 'primary' );
		echo '</form>';
	echo '</div>';
 
}

At the top of this method, we see the reference to $this->write_test_file(). This method actually writes the file to the server. If we find that this method returns true, that means that we are currently in the process of writing the file and that there is no need to output our test form.

The next line checks to see if the test file has been written. If the flag $this->test_file has been set to true, it means we have successfully written a file to the server and the notice will pop up. To avoid redundancy, it will only appear once immediately after the file has been written.

Finally, we actually output the test form. There is nothing special here – just your average options page HTML output. Since options pages aren’t really the focus of this tutorial, I’m going to skip down to the meat of the tutorial, the $this->write_test_file() method. Let’s have a look below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/**
* Writes a test file to a location of our determination using WP_Filesystem.
*
* @since 1.0.0
*
* @return bool
*/
private function write_test_file() {
 
	/** Don't do anything if the $_POST var isn't populated */
	if ( empty( $_POST ) )
		return false;
 
	/** Verify that our nonce is correct and that we initiated this action */
	check_admin_referer( 'tgm-wp-filesystem-nonce' );
 
	/** Make sure that the correct $_POST var is set before proceeding */
	if ( isset( $_POST['submit'] ) ) { // "submit" is the default name field for the submit_button function
		/** Prepare our variables for request_filesystem_credentials */
		$url = wp_nonce_url( add_query_arg( array( 'page' => 'tgm-wp-filesystem' ), 'options-general.php' ), 'tgm-wp-filesystem-nonce' );
		$method = ''; // Leave this blank so WP_Filesystem can populate it automatically
		$fields = array( 'submit' ); // Pass our input fields should we need to verify file ownership
 
		/** Let's try to setup WP_Filesystem */
		if ( false === ( $creds = request_filesystem_credentials( $url, $method, false, false, $fields ) ) )
			/** A form has just been output asking the user to verify file ownership */
			return true;
 
		/** If the user enters the credentials but the credentials can't be verified to setup WP_Filesystem, output the form again */
		if ( ! WP_Filesystem( $creds ) ) {
			/** This time produce the error that tells the user there was an error connecting */
			request_filesystem_credentials( $url, $method, true, false, $fields );
			return true;
		}
 
		/** If we get this far, WP_Filesystem has been setup successfully and the $wp_filesystem object has been populated */
		global $wp_filesystem;
 
		/** Let's write our test file, shall we? */
		$file_contents = __( 'This little bit of text will be written into our test text file.' );
		$this->filename = trailingslashit( $wp_filesystem->wp_content_dir() ) . 'tgm-wp-filesystem-test.txt';
 
		/** If there is an error, output a message for the user to see */
		if ( ! $wp_filesystem->put_contents( $this->filename, $file_contents, FS_CHMOD_FILE ) )
			echo '<div id="message" class="error"><p>' . __( 'There was an error writing your file. Please try again!' ) . '</p></div>';
		else
			$this->test_file = true; // Set the flag that our test file has been written
	}
 
	/** Return false if nothing has been submitted */
	return false;
 
}

What’s going on here? First, we check to make sure that something has actually been submitted to the $_POST var. If nothing has been submitted, the $_POST var will be empty, and there will be no need to proceed. This method will return false, and the normal test form on our options page will be output.

Next we use check_admin_referer to make sure that our nonces are correct. Without this check, this method could accidentally get fired from some other portion of the site and write the file without your permission. This quick check ensures that we actually clicked the “Write My Test File!” button.

If we have reached this point, we know that the $_POST var is not empty and that we have actually submitted the test form, so now let’s make sure that our particular form field was submitted. By default, submit_button uses the name attribute value “submit”, we so check to make sure that that particular key exists in the $_POST var before proceeding.

Phew – now we finally get to start preparing to initialize WP_Filesystem. There are 3 variables of importance here: $url, $method, and $fields.

If you have used a nonce in your form (which you should ALWAYS do), you need to pass the nonce in the URL where the form will be posted to. To do this, we use the wp_nonce_url to append our nonce to the end of our URL, which will be the URL of our options page.

$method determines the particular method in which you want WP_Filesystem to write files. By default, request_filesystem_credentials will determine the best method to use, so it’s best just to leave it blank. If you want to run some tests, you can manually enter the method, such as ‘ftp’.

$fields are an array of extra form fields that should be passed in the event that you need more information to verify file ownership. This ensures (to an extent) that no information is lost when having to enter extra credentials (note that this cannot accept input fields that store arrays of information, as listed in this trac ticket).

1
2
3
4
5
6
7
8
9
10
11
/** Let's try to setup WP_Filesystem */
if ( false === ( $creds = request_filesystem_credentials( $url, $method, false, false, $fields ) ) )
	/** A form has just been output asking the user to verify file ownership */
	return true;
 
/** If the user enters the credentials but the credentials can't be verified to setup WP_Filesystem, output the form again */
if ( ! WP_Filesystem( $creds ) ) {
	/** This time produce the error that tells the user there was an error connecting */
	request_filesystem_credentials( $url, $method, true, false, $fields );
	return true;
}

These two sections of code will initialize WP_Filesystem. The first line checks to see if request_filesystem_credentials can write files with the current credentials provided. If you have direct write access or have defined credentials such as FTP credentials in your wp-config.php file, $creds will be populated and the WP_Filesystem function will attempt to setup the $wp_filesystem object. If this isn’t the case, request_filesystem_credentials will spit out a form and the method will return true. Remember from earlier that when this method returns true, all output is stopped until we get further verification that we can actually write files to the server.

The second section of code attempt to initialize the object using the credentials provided (e.g. after you have filled out the form spit out by request_filesystem_credentials). If we can’t do it with the credentials provided, we will spit out the form again asking for more information and show the error to the user. This will keep occurring until correct credentials are provided.

1
2
3
4
5
6
7
8
9
10
11
12
/** If we get this far, WP_Filesystem has been setup successfully and the $wp_filesystem object has been populated */
global $wp_filesystem;
 
/** Let's write our test file, shall we? */
$file_contents = __( 'This little bit of text will be written into our test text file.' );
$this->filename = trailingslashit( $wp_filesystem->wp_content_dir() ) . 'tgm-wp-filesystem-test.txt';
 
/** If there is an error, output a message for the user to see */
if ( ! $wp_filesystem->put_contents( $this->filename, $file_contents, FS_CHMOD_FILE ) )
	echo '<div id="message" class="error"><p>' . __( 'There was an error writing your file. Please try again!' ) . '</p></div>';
else
	$this->test_file = true; // Set the flag that our test file has been written

Finally, the $wp_filesystem object has been populated if we reach this point! We can now begin writing files to the server!

$file_contents is just a variable to store the contents of our text, and $this->filename is the name of the file that we are going to use when writing the test file to the server. It contains the real path of the file in relation to your server.

$wp_filesystem->put_contents is the method that writes the file to the server. Upon success, it will return true, so we treat it is a boolean to determine the output. If successful, we set our $this->test_file flag to true so that the message will appear on our test form page. If unsuccessful, we output an error message.

1
2
/** Initialize the class */
$tgm_wp_filesystem = new TGM_WP_Filesystem;

Finally, at the end of the plugin we initialize the plugin class so that everything can run – and we are done!

The Complete Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
<?php
/*
Plugin Name: TGM WP_Filesystem Example
Plugin URI: http://thomasgriffinmedia.com/
Description: Provides a basic example for how to setup, initialize and use WP_Filesystem.
Author: Thomas Griffin
Author URI: http://thomasgriffinmedia.com/
Version: 1.0.0
License: GNU General Public License v2.0 or later
License URI: http://www.opensource.org/licenses/gpl-license.php
*/
 
/*  
	Copyright 2012  Thomas Griffin  (email : thomas@thomasgriffinmedia.com)
 
    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License, version 2, as 
    published by the Free Software Foundation.
 
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.
 
    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/
 
/**
 * Class for the WP_Filesystem example plugin.
 *
 * @since 1.0.0
 *
 * @package TGM_WP_Filesystem
 * @author Thomas Griffin <thomas@thomasgriffinmedia.com>
 */
class TGM_WP_Filesystem {
 
	/**
	 * Holds a copy of options pagehook.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	public $pagehook;
 
	/**
	 * Flag to determine if test file has been written.
	 *
	 * @since 1.0.0
	 *
	 * @var bool
	 */
	public $test_file = false;
 
	/**
	 * Constructor. Hooks all interactions into correct areas to start
	 * the class.
	 *
	 * @since 1.0.0
	 */
	public function __construct() {
 
		/** Load the plugin in the plugins_loaded hook */
		add_action( 'plugins_loaded', array( $this, 'init' ) );
 
	}
 
	/**
	 * Loads the plugin hooks and filters.
	 *
	 * @since 1.0.0
	 */
	public function init() {
 
		/** Create our options page */
		add_action( 'admin_menu', array( $this, 'options_page' ) );
 
	}
 
	/**
	 * Registers the options page with WordPress.
	 *
	 * @since 1.0.0
	 */
	public function options_page() {
 
		/** Register our options page and store the pagehook (if we needed) */
		$this->pagehook = add_options_page( __( 'TGM WP_Filesystem Options' ), __( 'TGM Options' ), 'manage_options', 'tgm-wp-filesystem', array( $this, 'options_page_cb' ) );
 
	}
 
	/**
	 * Callback function for the options page.
	 *
	 * @since 1.0.0
	 */
	public function options_page_cb() {
 
		/** If we are performing a WP_Filesystem action, return early - we don't need to display the extra information */
		if ( $this->write_test_file() )
			return;
 
		/** If our test file has been written and/or it exists, notify the user */
		if ( $this->test_file )
			echo '<div id="message" class="updated"><p>' . __( 'Your test file has been written successfully!' ) . '</p></div>';
 
		/** We aren't performing an action, so output a form */
		echo '<div class="wrap">';
			screen_icon( 'options-general' );
			echo '<h2>' . esc_html( get_admin_page_title() ) . '</h2>';
			echo '<p>' . __( 'The button below will be used to write a test file to your server. When the test file has been written successfully to your server, a notice will appear above this text. If more credentials are needed to verify file ownership, a form will be output asking for those credentials.' ) . '</p>';
			echo '<form action="" method="post">';
				/** Make sure we keep security first so we don't accidentally set this form off */
				wp_nonce_field( 'tgm-wp-filesystem-nonce' );
				submit_button( __( 'Write My Test File!' ), 'primary' );
			echo '</form>';
		echo '</div>';
 
	}
 
	/**
	 * Writes a test file to a location of our determination using WP_Filesystem.
	 *
	 * @since 1.0.0
	 *
	 * @return bool
	 */
	private function write_test_file() {
 
		/** Don't do anything if the $_POST var isn't populated */
		if ( empty( $_POST ) )
			return false;
 
		/** Verify that our nonce is correct and that we initiated this action */
		check_admin_referer( 'tgm-wp-filesystem-nonce' );
 
		/** Make sure that the correct $_POST var is set before proceeding */
		if ( isset( $_POST['submit'] ) ) { // "submit" is the default name field for the submit_button function
			/** Prepare our variables for request_filesystem_credentials */
			$url 	= wp_nonce_url( add_query_arg( array( 'page' => 'tgm-wp-filesystem' ), 'options-general.php' ), 'tgm-wp-filesystem-nonce' );
			$method = ''; // Leave this blank so WP_Filesystem can populate it automatically
			$fields = array( 'submit' ); // Pass our input fields should we need to verify file ownership
 
			/** Let's try to setup WP_Filesystem */
			if ( false === ( $creds = request_filesystem_credentials( $url, $method, false, false, $fields ) ) )
				/** A form has just been output asking the user to verify file ownership */
				return true;
 
			/** If the user enters the credentials but the credentials can't be verified to setup WP_Filesystem, output the form again */
			if ( ! WP_Filesystem( $creds ) ) {
				/** This time produce the error that tells the user there was an error connecting */
				request_filesystem_credentials( $url, $method, true, false, $fields );
				return true;
			}
 
			/** If we get this far, WP_Filesystem has been setup successfully and the $wp_filesystem object has been populated */
			global $wp_filesystem;
 
			/** Let's write our test file, shall we? */
			$file_contents 	= __( 'This little bit of text will be written into our test text file.' );
			$this->filename	= trailingslashit( $wp_filesystem->wp_content_dir() ) . 'tgm-wp-filesystem-test.txt';
 
			/** If there is an error, output a message for the user to see */
			if ( ! $wp_filesystem->put_contents( $this->filename, $file_contents, FS_CHMOD_FILE ) )
				echo '<div id="message" class="error"><p>' . __( 'There was an error writing your file. Please try again!' ) . '</p></div>';
			else
				$this->test_file = true; // Set the flag that our test file has been written
		}
 
		/** Return false if nothing has been submitted */
		return false;
 
	}
 
}
 
/** Initialize the class */
$tgm_wp_filesystem = new TGM_WP_Filesystem;

Final Thoughts

WP_Filesystem is an excellent way to ensure that your user will always be able to write files to their server, no matter what the setup. While it isn’t the easiest thing to understand in WordPress, it is definitely beneficial once you learn how to use it.

For some real life uses, I use it extensively in a class I’ve created TGM Plugin Activation. You can use that as a reference point for learning more about WP_Filesystem as well. I also use it in Soliloquy, my responsive slider plugin for WordPress, to handle the Addons page that allows users to download, install and activate addons straight from the dashboard!

I hope you have enjoyed this tutorial, and I look forward to your thoughts in the comments below!

  1. Elliott Stocks

    Nice article, I’ve always wondered about the file system. Thanks !

Comments are closed.