Custom WordPress Plugin Update Repository

April 18th, 2012  |  Published in Mind

I have recently been spending a lot of time developing the IssueM plugin for a company I do a lot of contract work for. IssueM is an issue manager plugin for WordPress, to manage issues for online magazines, periodicals, and such. The idea for IssueM started a couple years ago, and even started as a plugin, then morphed into a theme, and now it is back to a plugin. There was a lot of reasoning for these changes, but the biggest was that we wanted to make a plugin that could be used in any theme. Since the IssueM theme didn’t suit everyone’s needs, it was much easier to turn the functionality into shortcodes and widgets. Plus, since WordPress 3.1, we have all the capabilities we need to make a plugin extremely useful and successful in this arena.

Anyway, since this is a premium plugin we need a way to notify people of plugin updates. Frankly, I have never needed to do this, so I needed to figure it out. Luckily, I have a copy of Professional WordPress Plugin Development and in Chapter 9 there is very simple example of exactly what I wanted to do. Unluckily, the example doesn’t work… well, not entirely. It was always reporting an update, even if it was on the latest version. But I had enough information to trace down what was happening and figure out what I needed to change. By the way, the example code for the book is available from the publisher’s website.

So, without further adieu, this is a modified version of what I did:

This is the code I added to my plugin:

function my_plugin_plugins_api( $false, $action, $args ) {

	$plugin_slug = plugin_basename( __FILE__ );

	// Check if this plugins API is about this plugin
	if( $args->slug != $plugin_slug )
		return false;

	// POST data to send to your API
	$args = array(
		'action' 	=> 'get-plugin-information'
	);

	// Send request for detailed information
	$response = $this->my_plugin_api_request( $args );

	return $response;

}
add_filter( 'plugins_api', 'my_plugin_plugins_api', 10, 3 );

function my_plugin_update_plugins( $transient ) {

	// Check if the transient contains the 'checked' information
	// If no, just return its value without hacking it
	if ( empty( $transient->checked ) )
		return $transient;

	// The transient contains the 'checked' information
	// Now append to it information form your own API
	$plugin_path = plugin_basename( __FILE__ );

	// POST data to send to your API
	$args = array(
		'action' 	=> 'check-latest-version'
	);

	// Send request checking for an update
	$response = $this->my_plugin_api_request( $args );

	// If there is a new version, modify the transient
	if( version_compare( $response->new_version, $transient->checked[$plugin_path], '>' ) )
		 $transient->response[$plugin_path] = $response;

	return $transient;

}
add_filter( 'pre_set_site_transient_update_plugins', 'my_plugin_update_plugins' );

function my_plugin_api_request( $args ) {

	// Send request
	$request = wp_remote_post( http://api.url/location, array( 'body' => $args ) );

	if ( is_wp_error( $request ) || 200 != wp_remote_retrieve_response_code( $request ) )
		return false;

	$response = unserialize( wp_remote_retrieve_body( $request ) );

	if ( is_object( $response ) )
		return $response;
	else
		return false;

}

Then I setup a WordPress install for my API and created a custom template for the API page

/**
 * Template Name: API
**/

$action = $_REQUEST['action'];

// Create new object
$response = new stdClass;

switch( $action ) {

    // API is asked for the existence of a new version of the plugin
    case 'check-latest-version':
        $response->slug = 'my_plugin_slug';
        $response->new_version = '1.0.2';
        $response->url = 'http://plugin.url/';
        $response->package = 'http://plugin.url/download/location';
		break;

    // Request for detailed information
    case 'get-plugin-information':
        $response->name = 'my_plugin_name';
        $response->slug = 'my_plugin_slug';
        $response->requires = '3.3';
        $response->tested = '3.3.1';
	$response->rating = 100.0; //just for fun, gives us a 5-star rating :)
        $response->num_ratings = 1000000000; //just for fun, a lot of people rated it :)
        $response->downloaded = 1000000000; //just for fun, a lot of people downloaded it :)
        $response->last_updated = "2012-04-15";
        $response->added = "2012-02-01";
		$response->homepage = "http://plugin.url/";
        $response->sections = array(
            'description' =>  'Add a description of your plugin',
            'changelog' =>  'Add a list of changes to your plugin'
        );
        $response->download_link = 'http://plugin.url/download/location';
        break;

}

echo serialize( $response );

This isn’t exactly what I did, but it’s much more basic. Here are some things you can think about. First, the IssueM plugin uses an API key, so I send the API Key to api to verify the user has the right to update the plugin (say they don’t pay next year or something). The download_link setting is dynamic, for IssueM, it’s something like http://api.url/download/location?api=API_KEY. Then I have a separate script at the download location that will also verify the API Key and will send the latest IssueM plugin (which is located in a hidden directory). I also didn’t hard-code the downloaded argument, I actually track the downloads by using the dynamic download script. So my $response->downloaded actually equals get_option( ‘my_plugin_downloads’ ).

The download script I setup looks something like this:

/**
 * Template Name: Download Latest Version
 *
**/

$downloads = get_option( 'my_plugin_downloads', 100 );
update_option( 'my_plugin_downloads', ++$downloads );

$file = '/physical/location/of/your/file.zip';
$file_content = file_get_contents( $file );

header( 'Content-type: application/force-download' );
header( 'Content-Disposition: attachment; filename="my_plugin.zip"' );

echo $file_content;

exit;

You can completely customize and expand this code to do whatever you want. By adding extra arguments being sent to your API, you could have several plugins in this same repository. You can send API key information, host information, and anything else you’d need/want.

Tags: , , ,

Active Directory (LDAP) Authentication in WordPress Multi-Site

September 9th, 2010  |  Published in Mind

As many of you know, I led the team that launched the College of Education at UGA’s new website (http://www.coe.uga.edu/) which is driven by WordPress Multi-Site. The first phase is complete, and the second phase has started up. Part of their second phase is to allow a custodian from each department to edit content on their own departmental site. UGA uses Active Directory campus wide, so I thought it would be best to incorporate the WordPress authentication mechanism into UGA’s current AD implementation.

When I first researched this there weren’t many plugins for WordPress Multi-Site that handled AD/LDAP authentication. I was planning on writing one myself, but I found one yesterday by Clifton Griffin called Simple LDAP Login. I installed in on a development website, put in the necessary information and BAM it worked… but it wasn’t really designed for WordPress Multi-Site. What I needed was every site to use the Active Directory authentication, without having to set each site up individually. So I paired down the code a bit (to just what I needed) and stuck the files in the mu-plugins dir.

Clifton used the opensource PHP LDAP Class in his plugin, which works great. So, in my version, I stripped out everything I didn’t need/want like auto account creation, non-AD functionality, etc. Basically, I just needed to authenticate users via Active Directory.

So I stuck this code into a file in my mu-plugins folder. Now whenever anyone tries to authenticate it, uses Active Directory instead of WordPress’ user table:

<?php
require_once( WPMU_PLUGIN_DIR . '/simple-ldap-login/adLDAP.php' );

define( 'DOMAIN_CONTROLLERS', 	'active.directory.controller.domain' );
define( 'SECURITY_MODE', 		'high' );
define( 'ACCOUNT_SUFFIX', 		'@account.edu');
define( 'BASE_DN', 				'OU=OUOU,DC=DCDC,DC=EDU' );
define( 'USE_TLS',				false );
define( 'USE_SSL', 				true );

//Authenticate function
function sll_authenticate( $user, $username, $password ) {
	if ( is_a( $user, 'WP_User' ) ) { return $user; }

	//Failed, should we let it continue to lower priority authenticate methods?
	if( "high" === SECURITY_MODE ) {
		remove_filter('authenticate', 'wp_authenticate_username_password', 20, 3);
	}

	if ( empty( $username ) || empty( $password ) ) {
		$error = new WP_Error();

		if ( empty( $username ) )
			$error->add( 'empty_username', __( 'ERROR: The username field is empty.' ) );

		if ( empty($password) )
			$error->add( 'empty_password', __( 'ERROR: The password field is empty.' ) );

		return $error;
	}

	if( sll_can_authenticate( $username, $password ) ) {
			$user = get_userdatabylogin( $username );

			if ( !$user || ( strtolower( $user->user_login ) != strtolower( $username ) ) ) {
				do_action( 'wp_login_failed', $username );
				return new WP_Error( 'invalid_username', __( 'Login Error An error occurred while attempting to log in. If this continues please contact coeweb@uga.edu for support.' ) );
			} else {
				//we're ready to return the user
				return new WP_User( $user->ID );
			}
	} else {
		return new WP_Error( 'invalid_username', __( 'Login Error An error occurred while attempting to log in. If this continues please contact coeweb@uga.edu for support.' ) );
	}
}
add_filter( 'authenticate', 'sll_authenticate', 1, 3 );

function sll_can_authenticate( $username, $password ) {
	$sll_options = array(
		'account_suffix'		=>	ACCOUNT_SUFFIX,
		'base_dn'				=>	BASE_DN,
		'use_tls'				=>	USE_TLS,
		'use_ssl'				=>	USE_SSL,
		'domain_controllers'	=>	explode( ';', DOMAIN_CONTROLLERS )
	);

	$adldap = new adLDAP( $sll_options );

	$result = false;
	$result = $adldap->authenticate( $username, $password );

	return $result;
}

Tags: , , , , , ,

Notable Tech Posts – 2010.01.17

January 17th, 2010  |  Published in Mind

TiptTp jQuery plugin

Create a PHP  server load meter

jQuery Events: MouseOver – MouseOut vs MouseEnter – MouseLeave

15 useful jQuery plugins and tutorials

CSS important

Sexy jQuery drop down multi-level menu

jQuery quick guide

Take your HTML tables to a new level with Javascript frameworks

Javascript replace all using regex for

Tags: , , , , , , , , ,

Notable Tech Posts – 2010.01.10

January 10th, 2010  |  Published in Mind

Getting started with jQuery

Free furry cushions social icons set

WordPress shopping cart plugins

Converting your PHP app to MySQLi prepared statements

Autosuggest jQuery plugin

jQuery validation with regular expressions

Extending CSS with jQuery a new years guide

CSS 3D meninas -  just something cool to look at, pure CSS/HTML

jQuery animations a 7-step program

Implement password strength meter gauge JS

Colorful clock jQuery CSS

The best of the best jQuery resources

Cheat sheet series Javascript

MySQL: Using if in where

Tags: , , , , , , , ,

Notable Tech Posts – 2009.12.06

December 6th, 2009  |  Published in Mind

10 usability crimes you really shouldn’t commit

Design WordPress theme scratch

10 useful code snippets and plugins to spice up WordPress avatar

How to create a simple API with PHP and MySQL

Star Wars HTML and CSS a New Hope

10 ways to make WordPress more useful

10 front end techniques to improve your site usability

The ultimate toolbox for iPhone development

Premium free fresh WordPress themes year 2009

10 WordPress security plugins to keep your blog safe

jQuery slider tutorials and  plugins

Tags: , , , , , , , , , , ,