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: , , ,

Fun with SEO Spammers

January 26th, 2012  |  Published in Mind

I am involved in a joint venture with one of my close friends, Glenn Ansley. It’s the World’s Best Event Calendar Plugin for WordPress (in my humble opinion). Well, if you have a website, you know you’re going to get typical SPAM through your contact form, from so-called “SEO Experts”. We received one the other day that I thought was especially compelling, so much so that I decided to reply to him. Here is the brief email exchange…

Name: Carson White

Email: carsonwhite@sti-creative.com

Comments: Hi,

We carried out a preliminary analysis on your website and discovered the following areas of concern.
1.    Your website attracts limited traffic, which affects potential sales.
2.    Your keywords don’t feature in Google first page, which affects visibility.
3.    Your back links are not good enough, which affects link popularity.
4.    Your website is not properly promoted, which affects the overall score.
We are a Search Engine Optimization service provider and can assist you in overcoming the above mentioned problems. We can perform a more detailed analysis on your website and provide you the plan of action of how we can promote the same.

Please let us know if you require the SEO REPORT on your website at no cost.

Best Regards,
Carson
Systems Technology International Inc.
Michigan: 39555 Orchard Hill Place, Suite 461, Novi, Michigan-48375
New York: 9921 4th Ave, Brooklyn, NY 11209
www (dot) sti-cs (dot) com

Notice: Under The Bill 1618 Title iii passed by the 105th US Congress this mail may not be considered SPAM as long as the contact information is included and a method to be removed from our mailing list stated. If you are not interested in receiving our e-mails then please reply with REMOVE in the subject line and your ID will be removed from our mailing list. We apologize for the inconvenience caused to you.

And my reply…

Hi Carson,

I’ve carried out a preliminary analysis of your email and discovered the following areas of concern.
1.    You think we are suckers.
2.    You most likely implement black-hat “SEO” services that end up hurting companies, rather than helping them.
3     You probably over charge your clients for a service that none of them need. Google actually does a pretty good job of figuring out what is important and what isn’t… without people trying to game the system.
4.    You emailed us about our site not ranking well, but how do you explain finding our site?
5.    The bill S.1618 Title III was passed by the Senate, but was not passed by the House. It takes both the House and the Senate to pass a law. Furthermore, a similar bill, H.R. 3888 was passed by the House, but was not passed by the Senate, thus and again, not making it law. Therefore, your email is SPAM.

We are intelligent human beings and can assist you in overcoming the above mentioned problems. We can help develop a training in ethics, philosophy, and logic and provide you with a plan of action of how to stop SPAMMING and SCAMMING innocent businesses.

Please let us know if you require these lessons at much cost.

Best Regards,

Lew Ayotte
Full Throttle Development, LLC
http://fullthrottledevelopment.com
http://twitter.com/full_throttle
http://twitter.com/lewayotte

For some reason he never replied.

Tags: , , , ,

Globally Disable Comments and Pings in WordPress Multi-Site

October 7th, 2011  |  Published in Mind

Over the past couple years I’ve worked on a number of WordPress Multi-Sites that wanted to have their comments and pings disabled. In other words, they weren’t accepting comments and didn’t want them to be displayed. Normally this is easy to accomplish, simply turn off comments in the back end.

However, SPAMMERS knowing exactly how WordPress works and how to inject comment SPAM into WordPress. Though, if you do not have comments automatically approved, they definitely should not appear… if a comment does get injected, the WordPress administrator (and author) should receive a notification of a pending comment. So, there is a simple way in WordPress Multi-Site to just set all comment and ping submissions to false. Just stick this little bit of code into a php file in the mu-plugins directory:

add_filter( 'pings_open', '__return_false', 10, 2 );
add_filter( 'comments_open', '__return_false', 10, 2 );

One of the Multi-Site installations that I have this enabled in wanted to allow comments on two of their sites. If this is something you need, just setup a mu-plugin php file that looks like this:

function comments_and_pings_closed( $open, $post_id ) {

	global $blog_id;

	$comment_sites = array( 10, 15 ); // Site IDs of site that you don't want automatically disabled

	if ( in_array( $blog_id, $comment_sites ) ) {

		return $open;

	}

	return false;

}
add_filter( 'pings_open', 'comments_and_pings_closed', 10, 2 );
add_filter( 'comments_open', 'comments_and_pings_closed', 10, 2 );

Tags: , , , ,

How to search for user last name in the WordPress Users dashboard

May 3rd, 2011  |  Published in Mind

Have a client with almost 2500 users in their WordPress site and they wanted to be able to search their users by last name. By default this is not enabled in WordPress core, so here is a quick function/hook you can add to your functions.php file to enable this:

function custom_user_list_search( $query ) {

	if ( is_admin() ) {

		global $wpdb;

		if ( is_array( $qv = $query->query_vars ) && ( $s = trim( $qv['search'], '* \t\n\0\x0B' ) )
				&& ( $s = "%" . esc_sql( like_escape( $s ) ) . "%" ) ) {

			if ( !preg_match( '/' . $wpdb->usermeta . '/', $query->query_from ) )
				$query->query_from .= " INNER JOIN `" . $wpdb->usermeta . "` ON `" . $wpdb->users . "`.`ID` = `" . $wpdb->usermeta . "`.`user_id`";

			$query->query_where .= " OR ( `" . $wpdb->usermeta . "`.`meta_key` = 'last_name' AND `" . $wpdb->usermeta . "`.`meta_value` LIKE '" . $s . "' ) ";

		}

	}

	return $query;
}
add_filter( 'pre_user_query', 'custom_user_list_search', 15 );

I will probably expand this a little and create a WordPress plugin for it (if I can find the time).

NOTE: The trim regex should be this – ‘* \t\n\r\0\x0B’ – but the plugin I’m using is stripping the \r

Tags: , , ,

Rate Limiting with WordPress’ Transient API

November 4th, 2010  |  Published in Mind

I run a web app called leenk.me, it’s a Social Media Optimization application for WordPress. Basically it publishes your WordPress content to Twitter / Facebook / Google Buzz whenever you publish new content to your website. There are a lot of “advertisers” who have been signing up for the service and one in particular has been hitting the service hard — really hard — about 4000 API requests in an hour.

This is a pretty big problem, all these social networks rate limit their connections (close to 300 requests per hour). So making 4000 requests in an hour could cause them to ban the user, or worse, ban leenk.me. So I’ve had to implement a rate limit of my own. I thought about doing this with iptables to block anyone who exceeds a certain number of requests per hour, but the violating users wouldn’t know what happened. I really wanted needed a way to prevent them from over-connecting to the social networks.

This is where the WordPress Transients API comes in. The transients API is very similar to the Options API but with the added feature of an expiration time. This is basically how the transients API works in WordPress:

// Save a transient to the DB
set_transient( $transient_name, $transient_value, $expiration );
  • $transient_name – A unique identifier for your cached data.
  • $transient_value – Data to save, either a regular variable or an array/object. The API will handle serialization of complex data for you.
  • $expiration – Number of seconds to keep the data before refreshing.
// Get value of a transient from the DB
$transient_value = get_transient( $transient_name );
// Delete a transient from the DB
delete_transient( $transient_name );

The only problem with WP Transients is that there isn’t a good way to create “rolling transients” (as I call them). A rolling transient is a transient that rolls in time. In other words, I want to rate-limit a users connection by 1 hour from their current API call, but if the users does not make any API calls within the hour the transient should expire.

This is how I implemented “rolling transients”:

$call_limit = 350; // API calls (in an hour)
$time_limit = 60 * 60; // 1 hour (in seconds)
$transient_name = $host  . "_rate_limit"; // Using their host name as the unique identifier

// Check to see if there are any transients that match the name, if not create a new one
if ( false === ( $calls = get_transient( $transient_name ) ) ) {
	$calls[] = time();
	set_transient( $transient, $calls, $time_limit ); // Use an array of time() stamps for rolling effect
} else {
	// There is already a transient with this name
	$calls[] = time(); // Add a new time() stamp to the $calls array
	set_transient( $transient, $calls, $time_limit ); // Reset the transient (w/ expiration time)

	$call_count = count( $calls ); // How many calls have been made

	if ( $call_limit < $call_count ) { // If we're over the call limit, remove expired timestamps
		// Shift time from first element of array
		while ( $call = array_shift( $calls ) ) {
			// If time is >= current time - time limit, then it belongs in the array
			// Add it back and reset the transient
			if ( $call >= ( time() - $time_limit ) ) {
				array_unshift( $calls, $call );
				set_transient( $transient, $calls, $time_limit );
				break; // Stop processing, we're within the time_limit time now.
			}
		}

		// If we're still over the call limit, they've made too many requests in the time limit
		if ( $call_limit <= count( $calls ) ) {
			// The session needs to be killed
			die('Error: You have exceeded your rate limit for API calls, only ' . $call_limit . ' API calls are allowed every ' . $time_limit . ' seconds.');
		}
	}
}

With this code, you now have rolling transients... if your users exceed the number of calls within the past hour they will be rejected. If not, the transient will expire as it should. Let me know if you found this useful or if you have any tips for making it better (it seems a little "hacky" to me).

Tags: , ,