How does it work?
As of WordPress 4.5.2, the WP_Filesystem_FTPext
class is located at wp-admin/includes/class-wp-filesystem-ftpext.php. The FTP class allows the developer to connect to an FTP server and abstracts out some of the common FTP commands you may not be aware of.
With this class, you can do the following directly on the server you’re connected to:
- Get the contents of any file
- Get a contents array ( gets the contents of the file and separates each line into it’s own array key )
- Put the contents INTO a file
- List the directory you’re currently in
- Copy files to/from the FTP server
- More…
There are also some file attribute-related methods as well that allow you to:
- chmod a file
- Get the owner of a file
- Get the chmod of a file
- Get the group of a file
- Determine the size of the remote file
- Determine if you can read/write to that file
- More…
Simply put, this is a really robust class for abstracting typical FTP operations.
Okay, so what to do with it?
As you know, WordPress already utilizes this FTP class to do updates, install plugins, etc. However, in this special case, we needed to get the file from a remote server, insert it as an attachment, and later import that file.
The first immediate goal is to connect to FTP so we can get what we need. For the purposes of this article, we’re going to just use a simple singleton class. So here you go!
<?php class JW_Tests { /** * Instance of JW_Tests * @var JW_Tests */ public static $instance = null; public static function init() { if ( null == self::$instance ) { self::$instance = new self(); } return self::$instance; } public function hooks() { // All hooks here. add_action( 'init', array( $this, 'main' ) ); } public function main() { // Fancy stuff here } } function jw_tests() { return JW_Tests::init(); } add_action( 'plugins_loaded', array( jw_tests(), 'hooks' ) );
Now that we have the basic scaffolding setup, we need to jump into actually making the connection. We can do that in the main method of our new singleton.
I cannot stress this enough: You absolutely should NOT store the FTP credentials to this or ANY connection, either in the code, or even in the database. You have been warned!
We don’t want to fire this on EVERY init of WordPress, so adding a $_GET
flag will allow us to execute a download for testing purposes.
if ( ! isset( $_GET['download'] ) ) { return; }
Next, we have to make sure we have access to the classes we need, so we first check if these classes are available, and if not, we load them in. We’re going to need wp_tempnam()
specifically for this occasion. Since we obviously need the FTPext
class, it extends Filesystem_Base
, and we can’t have one without the other.
// First load the necessary classes if ( ! function_exists( 'wp_tempnam' ) ) { require_once( ABSPATH . 'wp-admin/includes/file.php' ); } if ( ! class_exists( 'WP_Filesystem_Base' ) ) { require_once( ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php' ); } if ( ! class_exists( 'WP_Filesystem_FTPext' ) ) { require_once( ABSPATH . 'wp-admin/includes/class-wp-filesystem-ftpext.php' ); }
Now, in the past I had issues with the FS_CONNECT_TIMEOUT
constant not being defined, so I went ahead and included that here (it’s required by the FTP class). I really didn’t see any sense in loading another entire class into memory just for one constant.
// Typically this is not defined, so we set it up just in case if ( ! defined( 'FS_CONNECT_TIMEOUT' ) ) { define( 'FS_CONNECT_TIMEOUT', 30 ); }
Once all that fancy stuff is done, and all checks have passed we setup our connection arguments, we can finally connect()
to the ftp server (or bail if for some reason the connection failed).
/** * You DO NOT want to hard-code this, these values are here specifically for * testing purposes. */ $connection_arguments = array( 'port' => 21, 'hostname' => '127.0.0.1', 'username' => '', 'password' => '', ); $connection = new WP_Filesystem_FTPext( $connection_arguments ); $connected = $connection->connect(); if ( ! $connected ) { return false; }
Great, we’re connected. Now what?
If you’re this far, you probably know of a file you want to download. Let’s download that file and import it as an attachment! Might want to grab a drink and a snack, because there’s going to be a decent amount of code ahead.
First things first, we need to make sure the file is indeed a file. To do this, we make use of the is_file()
method located in our connection we made previously, and if not, we bail:
$remote_file = "x.csv"; // Yep, you can use paths as well. // $remote_file = "some/remote-file.txt"; if ( ! $connection->is_file( $remote_file ) ) { return; }
Great, we have a file. Now you need to use the get_contents() method to grab the remote file’s contents into PHP memory, so we can push it elsewhere later.
// Get the contents of the file into memory. $remote_contents = $connection->get_contents( $remote_file ); if ( empty( $remote_contents ) ) { return; }
Now before I go further, let me rant a little.
For some context, let’s look at the get_contents()
method of the WP_Filesystem_FTPext
class.
The method does the following in order:
- Uses
wp_tempnam()
to create a temporary file - Opens the file with
fopen
, if not, it will bail - Runs
ftp_fget
(PHP’s native FTP method) to stream the remote file into the new temporary file, bail if it cannot - Rewinds the temp file pointer back to zero (kinda like a VHS tape…yeah, I said it!)
- Now, it reads the entire temp file into memory and returns it, kinda like
file_get_contents()
So, what’s my gripe? We don’t have access to this temp file name, at all. Sure, we could re-create the entire method of streaming the file just as the method currently does, but why re-invent the wheel if it’s already spinning? I smell a patch request coming! (View the WP_Filesystem_FTPext::get_contents() source to see what I mean.) You’ll see why this is a problem below.
Now that we have the remote contents in memory, we need to create our OWN file… despite the fact it was already created by core in the above step. We just can’t access it, which sucks. You’ll notice in the below snippet that we’re checking if the file is writable; this is because we’re also using file_put_contents()
to push our data in memory into the file. If, for some reason, pushing the content fails, or the temp file isn’t writable, we need to bail and cleanup after ourselves with unlink()
–be kind to the server.
// Create a temporary file to store our data. $temp_file = wp_tempnam( $remote_file ); if ( ! is_writable( $temp_file ) || false === file_put_contents( $temp_file, $remote_contents ) ) { unlink( $temp_file ); return; }
For those who may not know, unlink() will delete a file in PHP given the absolute path. Always make sure you clean up after yourself!
We need a bit more data for the side-load functionality we’re getting ready to dive into. The codex for wp_handle_sideload()
DOES actually hard-code the mime type, but in this example, I’m using wp_check_filetype()
so we can check the mime type against any extra registered types. If for some reason the type isn’t found for our file extension (in this case CSV), we gracefully exit, and again, clean up after ourselves.
// Optimally you want to check the filetype against a WordPress method, or you can hard-code it. $mime_data = wp_check_filetype( $remote_file ); if ( ! isset( $mime_data['type'] ) ) { // WE just don't have a type registered for this attachment unlink( $temp_file ); // Cleanup return; }
Woohoo, we’re almost there! Next up is the easy part: The side-load into the uploads directory.
These file arrays were copied from the sideload example in the codex; once again, no need to re-invent the wheel. That said, there are exceptions in the code. For instance, $temp_file
, which we created earlier, already exists, and we also grabbed the mime-type from core, instead of hard-coding it, and finally we can also use the pre-made file string which we defined above, as the name of the side-loaded file.
/** * The following arrays are pretty much a copy/paste from the Codex, no need * to re-invent the wheel. * @link https://codex.wordpress.org/Function_Reference/wp_handle_sideload#Examples */ $file_array = array( 'name' => basename( $remote_file ), 'type' => $mime_data['type'], 'tmp_name' => $temp_file, 'error' => 0, 'size' => filesize( $temp_file ), ); $overrides = array( 'test_form' => false, 'test_size' => true, 'test_upload' => true, ); // Side loads the content into the wp-content/uploads directory. $sideloaded = wp_handle_sideload( $file_array, $overrides ); if ( ! empty( $sideloaded['error'] ) ) { return; }
If no errors happened, you should get an array back like this from the wp_handle_sideload()
method:
array( 'file' => '/var/www/example.com/wp-content/uploads/2016/08/x.csv' 'url' => 'http://example.com/wp-content/uploads/2016/08/x.csv' 'type' => 'text/csv' )
Finally, we’re at the end of the line. Up to this point we’ve successfully downloaded a file from an FTP server, stored it as a temporary file, and utilized WordPress core methods to side-load it. But wait, there’s more….what?!?! I know that’s what you’re thinking! Bear with me here.
WordPress does not automatically ‘know’ files are in the uploads directory. This is what the database is for–so in order for you to use your newly downloaded file as an attachment, we need to create an attachment. It can be done in just a few lines of code:
// Will return a 0 if for some reason insertion fails. $attachment_id = wp_insert_attachment( array( 'guid' => $sideloaded['url'], // wp_handle_sideload() will have a URL array key which is the absolute URL including HTTP 'post_mime_type' => $sideloaded['type'], // wp_handle_sideload() will have a TYPE array key, so we use this in case it was filtered somewhere 'post_title' => preg_replace( '/\.[^.]+$/', '', basename( $remote_file ) ), // Again copy/paste from codex 'post_content' => '', 'post_status' => 'inherit', ), $sideloaded['file'] ); // wp_handle_sideload() will have a file array key, so we use this in case it was filtered // SUCCESSSSSSSSS!!!!!!!!!!!
This is where we make use of the array data we got back from wp_handle_sideload()
. After all, we want to use what WordPress gives us–if for some reason down the road something changes, or is filterable.
That’s all, folks!
So we’ve successfully downloaded from a remote FTP server and created an attachment for the downloaded file. This article only skims the surface of what’s possible with FTP downloads (especially in WordPress). You can do SO much more–download remote product CSV’s for import, FTP image downloads…just think about it!
Overall, I hope this at least opened your eyes to some possibilities and give you that spark of interest to create something awesome. At the very least I’d like to hear from you: What would you do differently? Did you like this article? Let me know! Let’s grow as developers, together!
Full source
You didn’t think I forgot to give you the source did you? Enjoy.