If you have a plugin hosted outside of the official WordPress repo (whether commercial or for clients), one issue you’ll inevitably run into is how to utilize automatic upgrades.

For my upcoming release of Ajax Edit Comments, I coded an upgrade class that immensely simplified the process for me.

First, I’ll share the code to the upgrade class and then go over implementation. Please note that this class is still a work in progress, and it being GPL, you’re welcome to expand on it. I do ask that if you do this, please share the changes with me so I can better improve the class.

Here are the current features of the class:

  1. Automatic updates work automagically upon successful instantiation.
  2. Filters to overwrite common parameters.
  3. Two public methods to check upgrades manually and perform remote requests.

The Upgrade Class

<?php
//Plugin Upgrade Class
if ( !class_exists( "PluginUpgrade" ) ) :
class PluginUpgrade {
			private $plugin_url = false;
			private $remote_url = false;
			private $version = false;
			private $plugin_slug = false;
			private $plugin_path = false;
			private $time_upgrade_check = false;
			private $plugins = '';
			
			function __construct( $args = array() ) {
				//Load defaults
				extract( wp_parse_args( $args, array( 
					'remote_url' => false,
					'version' => false,
					'plugin_slug' => false,
					'plugin_path' => false,
					'plugin_url' => false,
					'time' => 43200
				) ) );
				$this->plugin_url = $plugin_url;
				$this->remote_url = $remote_url;
				$this->version = $version;
				$this->plugin_slug = $plugin_slug;
				$this->plugin_path = $plugin_path;
				$this->time_upgrade_check = apply_filters( "pu_time_{$plugin_slug}", $time );
				
				//Get plugins for upgrading
				$this->plugins = $this->get_plugin_options();
				
				add_action( 'admin_init', array( &$this, 'init' ), 1 );
				add_action( "after_plugin_row_{$plugin_path}", array( &$this, 'plugin_row' ) );
				
				
			} //end constructor
			public function init() {
				//Set up update checking and hook in the filter for automatic updates
				//Do upgrade stuff
				//todo check is_admin() ?
				if (current_user_can("administrator")) {
					$this->check_periodic_updates();
					if ( isset( $this->plugins[ $this->plugin_slug ]->new_version ) ) {
						if( !version_compare( $this->version, $this->plugins[ $this->plugin_slug ]->new_version, '>=' ) ) {
							add_filter( 'site_transient_update_plugins', array( &$this, 'update_plugins_filter' ),1000 );
						}
					}
				}
			}
			//Performs a periodic upgrade check to see if the plugin needs to be upgraded or not
			private function check_periodic_updates() {	
				$last_update = isset( $this->plugins[ $this->plugin_slug ]->last_update ) ? $this->plugins[ $this->plugin_slug ]->last_update : false;
				if ( !$last_update ) { $last_update = $this->check_for_updates(); }
				if( ( time() - $last_update ) > $this->time_upgrade_check ){
						$this->check_for_updates();
				}
			} //end check_periodic_updates
			public function get_remote_version() {
				if ( isset( $this->plugins[ $this->plugin_slug ]->new_version ) ) {
					return $this->plugins[ $this->plugin_slug ]->new_version;
				}
				return false;
			}
			private function get_plugin_options() {
				//Get plugin options
				if ( is_multisite() ) {
					$options = get_site_option( 'pu_plugins' );
				} else {
					$options = get_option( 'pu_plugins' );
				}
				if ( !$options ) {
					$options = array();
				}
				return $options;
			}
			private function save_plugin_options() {
				//Get plugin options
				if ( is_multisite() ) {
					update_site_option( 'pu_plugins', $this->plugins );
				} else {
					$options = update_option( 'pu_plugins', $this->plugins );
				}
			}
			public function check_for_updates( $manual = false ) {
				if ( !is_array( $this->plugins ) ) return false;
				//Check to see that plugin options exist
				if ( !isset( $this->plugins[ $this->plugin_slug ] ) ) {

					$plugin_options = new stdClass;
					$plugin_options->url = $this->plugin_url;
					$plugin_options->slug = $this->plugin_slug;
					$plugin_options->package = '';
					$plugin_options->new_version = $this->version;
					$plugin_options->last_update = time();
					$plugin_options->id = "0";
					
					$this->plugins[ $this->plugin_slug ] = $plugin_options;
					$this->save_plugin_options();
				}
												

				$current_plugin = $this->plugins[ $this->plugin_slug ];
				if( ( time() - $current_plugin->last_update ) > $this->time_upgrade_check || $manual ) {
					//Check for updates
					$version_info = $this->perform_remote_request( 'get_version' );
					if ( is_wp_error( $version_info ) ) return false;
					//$version_info should be an array with keys ['version'] and ['download_url'] 
					if ( isset( $version_info->version ) && isset( $version_info->download_url ) ) {
						$current_plugin->new_version = $version_info->version;
						$current_plugin->package = $version_info->download_url;
						$this->plugins[ $this->plugin_slug ] = $current_plugin;
						$this->save_plugin_options();
					}
				}
				return $this->plugins[ $this->plugin_slug ];
			} //end check_for_updates
			
			public function plugin_row( $plugin_name ) {
				do_action( "pu_plugin_row_{$this->plugin_slug}", $plugin_name );
			}
			public function perform_remote_request( $action, $body = array(), $headers = array(), $return_format = 'json' ) {
			
				$body = wp_parse_args( $body, array( 
					'action' => $action,
					'wp-version' => get_bloginfo( 'version' ),
					'referer' => site_url()
				) ) ;
				$body = http_build_query( apply_filters( "pu_remote_body_{$this->plugin_slug}", $body ) );
				
				$headers = wp_parse_args( $headers, array( 
					'Content-Type' => 'application/x-www-form-urlencoded',
					'Content-Length' => strlen( $body )
				) );
				$headers = apply_filters( "pu_remote_headers_{$this->plugin_slug}", $headers );
				
				$post = array( 'headers' => $headers, 'body' => $body );
				//Retrieve response
				$response = wp_remote_post( esc_url( $this->remote_url ), $post );
				$response_code = wp_remote_retrieve_response_code( $response );
				$response_body = wp_remote_retrieve_body( $response );
				
				if ( $response_code != 200 || is_wp_error( $response_body ) ) {
					return false;
				}
				
				if ( $return_format != 'json' ) {
					return $response_body;
				} else {
					return json_decode( $response_body );
				}
				return false;
			} //end perform_remote_request
		
		//Return an updated version to WordPress when it runs its update checker
		public function update_plugins_filter( $value ) {
			if ( isset( $this->plugins[ $this->plugin_slug ] ) && $this->plugin_path ) {
				$value->response[ $this->plugin_path ] = $this->plugins[ $this->plugin_slug ];
			}
			return $value;
		}
		
} //end class
endif;
?>

Instantiating the Class

Somewhere in your plugin, you would instantiate the class (assuming you’ve included the class in with your plugin):

<?php
$args = array(
	'remote_url' => 'http://domain.com/remoteupgrade.php',
	'version' => '4.2.0.0',
	'plugin_slug' => 'wp-ajax-edit-comments',
	'plugin_path' => 'wp-ajax-edit-comments/wp-ajax-edit-comments.php',
	'plugin_url' => 'http://www.ajaxeditcomments.com',
	'time' => 43200
);
				
$upgrade = new PluginUpgrade( $args );
?>

The class accepts six parameters upon instantiation.

  1. remote_url: A URL that the upgrader will ping to retrieve update information. I’ll get into this in a bit.
  2. version: A string version number of the current plugin installed.
  3. plugin_slug: A unique slug that identified your plugin.
  4. plugin_path: The path to your plugin from the /plugins/ directory.
  5. plugin_url: The URL to your plugin.
  6. time: An integer in seconds that dictates how long to wait between upgrade checks. Default is 12 hours (or 43200 seconds).

On the remote side of it, assuming a URL with the http://domain.com/remoteupgrade.php path, you’ll need to return a JSON string of the new plugin’s version and a download URL.

When sending an update request, the class automatically passes an action POST variable to the remote_url. The default action for checking for upgrades is get_version.

At the remote_url location, you would do a check for an action variable and do something based on the action.

Here’s an example:

<?php
$action = $_POST['action'];
switch ( $action ) {
	case 'get_version': 
		echo json_encode( array( 
			'version' => '4.3.0.0',
			'download_url' => 'http://domain.com/downloads/wp-ajax-edit-comments.zip'
			) );
		break;
	case 'some_other_action':
		//...//
		break
} //end switch
?>

The upgrade class assumes two JSON variables being returned: version and download_url. How you get those values in your remote_url is up to you.

Public Methods

The class has two public methods to assist in performing other requests besides checking for plugin updates.

perform_remote_request

The perform_remote_request method allows you to perform an arbitrary remote request on the remote_url you specified. Again, this is action based.

Here’s an example:

$result = $upgrade->perform_remote_request( 'authkey_check', array( 'key' => $key ) );

The first argument is an action name. The second and third arguments are optional, and allow you to set header/body arguments respectively. A fourth argument is also optional, and allows you to specify the return format (e.g., json).

Here’s an example I have of retrieving a remote changelog:

$result = $upgrade->perform_remote_request( 'changelog', array(), array(), 'html' );

check_for_updates

If your plugin has an option to check for updates manually (without having to wait until the magic 12 hour mark), you can call the check_for_updates method. Just pass a true to the method to skip the time check.

$plugin_info = $upgrade->check_for_updates( true );

Class Hooks

  • pu_remote_body_{$plugin_slug} – Filter – Allows you to add default body arguments for the remote request
  • pu_remote_headers_{$plugin_slug} – Filter – Allows you to add default header arguments for the remote request
  • pu_time_{$plugin_slug} – Filter – Allows you to overwrite the default time between upgrade checks

Conclusion

In summary, the class allows you to:

  1. Automagically set up automatic upgrades upon instantiation. This requires you to pass some parameters back based on your remote_url.
  2. Perform manual upgrade checks.
  3. Perform arbitrary remote requests to your remote_url.
  4. Set default headers/body arguments per filters.

I hope this can help get the ball rolling for other plugin authors.

2 Responses to “WordPress Upgrade Class for Commercial or Client Plugins”

  1. Kaiser March 15, 2011 at 1:27 pm #

    Hi Ronald,

    just stumbled upon your class. I reworked it today only for readbility [1], but will look through it and contribute further. Goal is to make it available for themes too. I will likely rename some of the vars and function names too (too long imho).

    Thanks for your work. I’ll keep you updated.

    [1] https://gist.github.com/871274

    • Ronald Huereca March 16, 2011 at 2:34 pm #

      Thanks or the update. Code looks good. Keep up the good work.

      This is exactly why I posted it. Hopefully you’ll find it useful.