Skip to content

Instantly share code, notes, and snippets.

@GaryJones
Created July 10, 2015 11:03
Show Gist options
  • Select an option

  • Save GaryJones/a616ff5319cf1b549284 to your computer and use it in GitHub Desktop.

Select an option

Save GaryJones/a616ff5319cf1b549284 to your computer and use it in GitHub Desktop.
EDD-VAT MU updater file
<?php
/**
*
* Version: 1.0.7
*
* This must-use plugin implements three things:
*
* 1) A class that will be used indirectly by a host plugin to
* check to see if this plugin already exists and, if not,
* will copy this file to the mu-plugins folder.
*
* 2) A filter for a request to retrieve a list of the active
* plugins using get_option(). If the WordPress installation
* uses a multi-site configuration and the current user is network
* administrator, the filter will return all plugins that are
* active in any blog which also implement an SL Updater or include
* properties which allows them to be updated.
*
* 3) This plugin also implements a version of the updater plugin
* created by Pippin Williamson which is heavily modified to support
* updating plugins which are not network activated and are not in
* the number 1 blog.
*
* How does WordPress collect and display plugin updates?
*
* The presentation of plugin updates is the result of several
* independent processes
*
* Plugins are checked for updates by the function wp_update_plugins
* defined in wp_includes/update.php. wp_update_plugins is called
* in many ways for example by scheduling.
*
* As it processes, wp_update_plugins calls set_site_transient()
* with an argument of 'update_plugins'. As it begins, this function
* runs the filter pre_set_site_transient_update_plugins. This is
* filter added in the lsl_sl_plugin_updater class in this file.
* The filter implementation in the lsl_sl_plugin_updater class
* attempts to retrieve update information from the vendor site.
*
* If plugin information can be retrieved, the filter checks the
* returned version number and, if greater than version of the
* uninstalled plugin, the updated information contained in the
* response from the vendor is added to the 'response' array of
* the transient data passed to the filter which is keyed by the
* plugins file name (technically the result of a call to the
* WordPress function plugin_basename()).
*
* When the filter returns to the set_site_transient() function
* the transient data, which now contains information for any
* updated plugin, is saved as an option with the option_name
* '_set_site_transient_update_plugins'.
*
* Available plugins are displayed by the table WP_Plugins_List_Table
* which is defined in class-wp-plugins-list-table.php. It can be
* seen from the prepare() and display() methods that the content
* for the table are the entries in the global $plugins variable.
*
* The display function of the table class calls the 'single_row'
* function render each row. This function in turn runs the
* action 'after_plugin_row_$plugin_file' where $plugin_file
* contains the file name (again, technically the result of a
* call to the WordPress function plugin_basename()). So the action
* run will be something like:
*
* after_plugin_row_my_plugin/my_plugin.php
*
* It's clear that the action name is plugin specific but plugins
* don't usually include an action with this name so where are these
* actions defined?
*
* The answer is the function wp_plugin_update_rows() which is
* defined in wp-admin\includes\update.php. This function is
* called by an action attached to 'admin_init'.
*
* This function retrieves the '_set_site_transient_update_plugins'
* options record, deserializes the value and sets up an action for
* every entry in the deserialized object which has an entry in the
* 'response' array. This is the array updated by the
* pre_set_site_transient_update_plugins filter of the
* lsl_sl_plugin_updater class when it is determined that the plugin
* version number has changed.
*
* To recap:
*
* Plugin update information is collected based on several events
* and any change information is stored as a site transient option.
*
* An action is setup for every entry in the transient that holds
* plugin change information.
*
* The action is used by the single_row function of the
* WP_Plugins_List_Table class to render a notice about the
* respective plugin change.
*
* Is this perfect? No probably not. A point of potential cause
* for confusion when creating a plugin that is updateable arises
* because action which runs to add the actions which will render
* change information executes *before* the call is made to request
* update information. As a result, any changes do not appear when
* the plugins are first displayed. Instead they appear the second
* time the list of plugin is displayed. To the end user this does
* not matter. They will not expect a plugin they've just installed
* to have been updated. However the plugin developer who is
* testing to confirm updating does work may be confused if the
* fact of a change does not appear immediately.
*
* This could be resolved by calling wp_update_plugins() before
* wp_plugin_update_rows() is called. However deferring the use
* of wp_update_plugins() means the set_site_transient() function
* can be used. The benefit of using the transient function means
* their inherent expiration feature can be exploited to prevent
* update information being requested every time plugins are listed
* which would be very intrusive.
*
* @author Bill Seddon, Lyquidity Solutions
* @version 1.0.7
* @copyright (c) Lyquidity Solutions Limited
*/
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) exit;
// Only include this function if the file is not in the mu-plugins folder
if (is_multisite() && dirname(__FILE__) != realpath(WPMU_PLUGIN_DIR) && !class_exists('lsl_mu_updater'))
{
// This class is responsible for copying this file to the MU plugins folder if in a mult-site
class lsl_mu_updater {
/**
* @var lsl_mu_updater The one true lsl_mu_updater
* @since 1.0
*/
private static $instance;
public $slug = "";
public $folder = "";
/**
* Main lsl_mu_updater instance
*
* Insures that only one instance of lsl_mu_updater exists in memory at any one
* time. Also prevents needing to define globals all over the place.
*
* @since 1.0
* @static
* @staticvar array $instance
*/
public static function instance() {
if ( ! isset( self::$instance ) && ! ( self::$instance instanceof lsl_mu_updater ) ) {
self::$instance = new lsl_mu_updater;
}
return self::$instance;
}
/**
* PHP5 constructor method.
*
* @since 1.0
*/
public function __construct() {
}
function actions()
{
add_action('admin_notices', array($this, 'lsl_mu_updater_notices'));
}
function lsl_mu_updater_notices()
{
$message = get_transient("{$this->slug}-mu-updater");
delete_transient("{$this->slug}-mu-updater");
if (empty($message)) return;
$class = $message[0] == '!'
? "error"
: "updated";
if ($message[0] == '!') $message = substr($message, 1);
echo "<div class='{$class}'><p>{$message}</p></div>";
}
/**
* Activation method
*/
function lsl_mu_updater_activation()
{
$file = __FILE__;
// Can't rely on __FILE__ because this class might have been loaded first by a different plugin
// So we'll hard code the file source updater to be in the plugin folder
$file = WP_PLUGIN_DIR . "/{$this->folder}/" . basename(__FILE__);
$WPMU_PLUGIN_DIR = WPMU_PLUGIN_DIR;
if ( is_dir( $WPMU_PLUGIN_DIR ) )
{
$mu = wp_get_mu_plugins();
// If the plugin already exists, there's nothing to do
// TODO check to see if the existing version needs to be updated
if (in_array($WPMU_PLUGIN_DIR . '/' . basename($file), $mu))
{
// Get the headers for this file
$headers = get_file_data($file, array('Version' => 'Version'));
$muheaders = get_file_data($WPMU_PLUGIN_DIR . '/' . basename($file), array('Version' => 'Version'));
// returns -1 if the first version is lower than the second, 0 if they are equal, and 1 if the second is lower
if (version_compare($muheaders['Version'], $headers['Version']) >= 0)
return true;
}
}
else
{
// Attempt to create the mu folder
if(!mkdir($WPMU_PLUGIN_DIR, '0755'))
{
$message = "!Failed to create the mu plugins folder ({$WPMU_PLUGIN_DIR})";
set_transient("{$this->slug}-mu-updater", $message, 10);
return false;
}
}
if (!copy($file, $WPMU_PLUGIN_DIR . "/" . basename($file)))
{
$message = "!Unable to copy the mu-updater plugin file ($file} to the mu-plugins folder '{$WPMU_PLUGIN_DIR}'";
set_transient("{$this->slug}-mu-updater", $message, 10);
return false;
}
$message = "Must-use plugin file copied to the mu-plugins folder";
if (!empty($message))
set_transient("{$this->slug}-mu-updater", $message, 10);
}
function register($plugin)
{
$this->slug = basename( $plugin, '.php');
$this->folder = dirname( $plugin );
// error_log("register {$this->folder}");
register_activation_hook( $plugin, array($this, 'lsl_mu_updater_activation' ) );
}
} // class lsl_mu_updater
} // if class_exists('lsl_mu_updater')
// Only include the lsl_updater_get_options function and the
// updater class if the file *is* in the mu-plugins folder
if (dirname(__FILE__) == realpath(WPMU_PLUGIN_DIR))
{
// error_log('pre_option_active_plugins');
add_filter('pre_option_active_plugins', 'lsl_updater_get_options', 10, 1);
function lsl_updater_get_options($default)
{
global $wpdb;
if (is_network_admin() && is_multisite())
{
$option = 'active_plugins';
$edd_folder = 'easy-digital-downloads/easy-digital-downloads.php';
if (isset($GLOBALS["{$option}_processed"])) return $GLOBALS["{$option}_processed"];
wp_cache_add( $option, null, 'options' );
/* error_log('is_network_admin');
$backtrace = debug_backtrace();
foreach ( $backtrace as $trace )
{
error_log(
(isset($trace['file']) ? $trace['file'] . "," : "") .
(isset($trace['line']) ? $trace['line'] : "") . " (" .
(isset($trace['function']) ? $trace['function'] : "") . ', ' .
(isset($trace['class']) ? $trace['class'] : "(no class)") . ")"
);
}
*/
$current_blog_options = array();
$row = $wpdb->get_row( $wpdb->prepare( "SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1", $option ) );
if ( is_object( $row ) )
{
$value = $row->option_value;
$current_blog_options = maybe_unserialize( $value );
}
$other_blog_options = array();
// 'get-blog_list' is deprecated (it's slow because it retrieves post
// details for the blog) so get the blog list explicitly.
$query = "SELECT blog_id, domain, path " .
"FROM $wpdb->blogs " .
"WHERE site_id = %d AND " .
" public = '1' AND " .
" archived = '0' AND " .
" mature = '0' AND " .
" spam = '0' AND " .
" deleted = '0' " .
"ORDER BY registered ASC";
$blogs = $wpdb->get_results( $wpdb->prepare($query, $wpdb->siteid), ARRAY_A );
$plugins_encountered = array();
// The process now is to set the blog id of the $wpdb instance and
// retrieve options for each one in turn looking for a license key.
// Keep a copy of the current blog so it's options can be skipped
// as the answer is already known and so the current blog id can be
// reset at the end.
$current_blog_id = $wpdb->blogid;
foreach ( (array) $blogs as $details ) {
// Not interested in the current blog
if ($current_blog_id == $details['blog_id'])
continue;
// Causes all subsequent wpdb requests to use table names
// appropriate for the blog
$wpdb->set_blog_id($details['blog_id']);
$row = $wpdb->get_row( $wpdb->prepare( "SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1", $option ) );
if ( !is_object( $row ) ) continue;
$value = maybe_unserialize( $row->option_value );
$value = array_filter(
maybe_unserialize( $row->option_value ),
function($item) use($edd_folder, &$plugins_encountered) {
// Only process each plugin once
if (in_array($item, $plugins_encountered)) return false;
$plugins_encountered[] = $item;
// Only include those plugins that have an updater or *is* EDD...
if (!file_exists( WP_PLUGIN_DIR . '/' . $item )) return false;
// If EDD just take it
if ($item == $edd_folder) return true;
// If not look for an updater
if (!file_exists( WP_PLUGIN_DIR . '/' . dirname($item) . '/edd_mu_updater.php'))
{
// ...or if marked as SL updateable
$headers = get_file_data(WP_PLUGIN_DIR . '/' . $item, array('updateable' => 'Updateable'));
if (!isset($headers['updateable']) || !(boolean)$headers['updateable']) return false;
}
// setup the updater
$updater = new lsl_sl_plugin_updater(
'',
WP_PLUGIN_DIR . '/' . $item,
array(
'blog_url' => get_bloginfo( 'url' )
)
);
return true;
});
$other_blog_options = array_unique(array_merge($other_blog_options, $value));
}
$wpdb->set_blog_id($current_blog_id);
wp_cache_set( 'alloptions', null, 'options' );
unset( $blogs );
// If the edd plugin does not exist then default to the current blog plugins
$result = in_array($edd_folder, $current_blog_options) || in_array($edd_folder, $other_blog_options)
? array_unique(array_merge($current_blog_options, $other_blog_options))
: $current_blog_options;
sort($result);
wp_cache_add( $option, $result, 'options' );
$GLOBALS["{$option}_processed"] = $result;
return $result;
}
return false;
}
}
if (!class_exists('lsl_sl_plugin_updater'))
{
// uncomment this line for testing
// set_site_transient( 'update_plugins', null );
/**
* Allows plugins to use their own update API.
* Modified version of the EDD_SL_Plugin_Updater
* class by Pippin Williamson to work with a
* WordPress multi-site configuration
*
* @author Bill Seddon
* @version 1.0
*/
class lsl_sl_plugin_updater {
private $api_url = '';
private $api_data = array();
private $name = '';
private $slug = '';
/**
* Class constructor.
*
* @uses plugin_basename()
* @uses hook()
*
* @param string $_api_url The URL pointing to the custom API endpoint.
* @param string $_plugin_file Path to the plugin file.
* @param array $_api_data Optional data to send with API calls.
* @return void
*/
function __construct( $_api_url, $_plugin_file, $_api_data = null ) {
$this->api_url = empty($_api_url) ? "" : trailingslashit( $_api_url );
$this->api_data = $_api_data;
$this->name = plugin_basename( $_plugin_file );
$this->slug = basename( $_plugin_file, '.php');
$this->version = isset($_api_data['version']) ? $_api_data['version'] : '';
$this->author = isset($_api_data['author']) ? $_api_data['author'] : '';
$this->blog_url = isset($_api_data['blog_url']) ? $_api_data['blog_url'] : '';
$this->plugin = plugin_basename( $_plugin_file );
// Set up hooks.
$this->hook();
}
/**
* Set up Wordpress filters to hook into WP's update process.
*
* @uses add_filter()
*
* @return void
*/
private function hook() {
add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'pre_set_site_transient_update_plugins_filter' ) );
add_filter( 'plugins_api', array( $this, 'plugins_api_filter' ), 10, 3 );
add_filter( 'http_request_args', array( $this, 'http_request_args' ), 10, 2 );
}
/**
* Disable SSL verification in order to prevent download update failures
*
* @param array $args
* @param string $url
* @return object $array
*/
function http_request_args( $args, $url ) {
// If it is an https request and we are performing a package download, disable ssl verification
if( strpos( $url, 'https://' ) !== false && strpos( $url, 'edd_action=package_download' ) ) {
$args['sslverify'] = false;
}
return $args;
}
/**
* Check for Updates at the defined API endpoint and modify the update array.
*
* This function dives into the update api just when Wordpress creates its update array,
* then adds a custom API call and injects the custom plugin data retrieved from the API.
* It is reassembled from parts of the native Wordpress plugin update code.
* See wp-includes/update.php line 121 for the original wp_update_plugins() function.
*
* @uses api_request()
*
* @param array $_transient_data Update array build by Wordpress.
* @return array Modified update array with custom plugin data.
*/
function pre_set_site_transient_update_plugins_filter( $_transient_data ) {
// error_log("pre_set_site_transient_update_plugins_filter - " . $this->slug);
if( empty( $_transient_data ) ) return $_transient_data;
$to_send = array( 'slug' => $this->slug );
if (isset($this->blog_url)) $to_send['url'] = $this->blog_url;
$api_response = $this->api_request( 'plugin_latest_version', $to_send );
if( false !== $api_response && is_object( $api_response ) && isset( $api_response->new_version ) ) {
if( version_compare( $this->version, $api_response->new_version, '<' ) )
{
// error_log("Changed: name: {$this->name} slug: {$this->slug}");
$_transient_data->response[$this->name] = $api_response;
}
}
return $_transient_data;
}
/**
* Updates information on the "View version x.x details" page with custom data.
*
* @uses api_request()
*
* @param mixed $_data
* @param string $_action
* @param object $_args
* @return object $_data
*/
function plugins_api_filter( $_data, $_action = '', $_args = null ) {
if ( ( $_action != 'plugin_information' ) || !isset( $_args->slug ) || ( $_args->slug != $this->slug ) )
return $_data;
// error_log("plugins_api_filter $_action - " . $this->slug);
$to_send = array( 'slug' => $this->slug );
if (isset($this->blog_url)) $to_send['url'] = $this->blog_url;
$api_response = $this->api_request( 'plugin_information', $to_send );
if ( false !== $api_response )
{
if (isset($api_response->compatibility))
$api_response->compatibility = maybe_unserialize($api_response->compatibility);
$_data = $api_response;
}
return $_data;
}
/**
* Calls the API and, if successful, returns the object delivered by the API.
*
* @uses get_bloginfo()
* @uses wp_remote_post()
* @uses is_wp_error()
*
* @param string $_action The requested action.
* @param array $_data Parameters for the API action.
* @return false||object
*/
private function api_request( $_action, $_data ) {
global $wp_version;
// error_log(print_r($this->api_data,true));
$data = array_merge( $this->api_data, $_data );
if( $data['slug'] != $this->slug )
return false;
// If you are running the 'Network' site in a multi-site configuration
// and the application is activated only for one site then its not
// active for the network and so not licensed for the network.
// As a result the license will be empty and it's necessary to find the
// license from one of the constituent blogs.
if (is_multisite() && is_network_admin())
if( empty( $data['license']) || empty($data['item_name']) || empty($this->api_url))
{
global $wpdb;
// 'get-blog_list' is deprecated (it's slow because it retrieves post
// details for the blog) so get the blog list explicitly.
$query = "SELECT blog_id, domain, path " .
"FROM $wpdb->blogs " .
"WHERE site_id = %d AND " .
" public = '1' AND " .
" archived = '0' AND " .
" mature = '0' AND " .
" spam = '0' AND " .
" deleted = '0' " .
"ORDER BY registered ASC";
$blogs = $wpdb->get_results( $wpdb->prepare($query, $wpdb->siteid), ARRAY_A );
// The process now is to set the blog id of the $wpdb instance and
// retrieve options for each one in turn looking for a license key.
// Keep a copy of the current blog so it's options can be skipped
// as the answer is already known and so the current blog id can be
// reset at the end.
$current_blog_id = $wpdb->blogid;
foreach ( (array) $blogs as $details ) {
// Not interested in the current blog
if ($current_blog_id == $details['blog_id'])
continue;
// Causes all subsequent wpdb requests to use table names appropriate
// for the blog
$wpdb->set_blog_id($details['blog_id']);
// Clear an existing options cache or the existing options will
// be returned
wp_cache_set( 'alloptions', null, 'options' );
// Should probably check to see if the plugin is active
// It might be done using this code. Can't use the get_options()
// function because it's the function above which return active
// plugins across *all* blogs
$row = $wpdb->get_row( $wpdb->prepare( "SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1", 'active_plugins' ) );
if ( !is_object( $row ) ) continue;
$active_plugins = maybe_unserialize( $row->option_value );
if (!is_array($active_plugins) || !in_array($this->plugin, $active_plugins)) continue;
$filters = apply_filters('sl_updater_' . $this->slug, array(), array('license','item_name','api_url','version','author'));
$result = wp_parse_args( $filters, $data);
// No license or product name or url? Then move along, nothing to see
if (empty($result['license']) ||
empty($result['item_name']) ||
empty($result['version']) ||
(empty($this->api_url) && empty($result['api_url']))
) continue;
// Yes? The grab it and get out of here. Only one license is needed to force the update.
if (empty($this->api_url)) $this->api_url = $result['api_url'];
$data['license'] = $result['license'];
$data['item_name'] = $result['item_name'];
$data['version'] = $this->version = $result['version'];
$data['author'] = $this->author = (isset($result['author']) ? $result['author'] : "");
$data['blogid'] = $details['blog_id'];
// Grab the url of the blog as well at it forms the user-agent header
$data['url'] = get_bloginfo( 'url' );
break;
}
$wpdb->set_blog_id($current_blog_id);
wp_cache_set( 'alloptions', null, 'options' );
unset( $blogs );
}
if( empty( $data['license'] ) )
return false;
// error_log("Got license: {$data['license']}");
$api_params = array(
'edd_action' => 'get_version',
'license' => $data['license'],
'name' => htmlentities( $data['item_name'] ),
'slug' => $this->slug,
'author' => $this->author
);
if (!isset($data['url']) || empty($data['url'])) $data['url'] = get_bloginfo('url');
// Create a user-agent header because WordPress uses the 'network' blog url by default
$useragent = apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . $data['url']);
$args = array( 'user-agent' => $useragent, 'timeout' => 15, 'sslverify' => false, 'body' => $api_params );
$request = wp_remote_post( $this->api_url, $args );
/* If successful the response will be an array which contains a
* [body] element which is a serialized array containing:
*
* new_version
* name
* version
* slug
* author
* url
* homepage
* package Link to a download
* sections []
* description The download content
* changelog
*/
if ( !is_wp_error( $request ) ):
$request = json_decode( wp_remote_retrieve_body( $request ) );
// error_log("Request");
// error_log(print_r($request,true));
if( $request && isset( $request->sections ) )
$request->sections = maybe_unserialize( $request->sections );
return $request;
else:
return false;
endif;
}
}
}
if (!function_exists('init_lsl_mu_updater'))
{
function init_lsl_mu_updater($plugin) {
// The mu-plugin should only be updated in a multi-site configuration
if (!is_multisite()) return null;
if (!class_exists('lsl_mu_updater'))
{
error_log("Class does not exist for '$plugin'");
return null;
}
$updater = lsl_mu_updater::instance();
$updater->actions();
$updater->register($plugin);
return $updater;
}
}
if (!function_exists('init_lsl_mu_updater2'))
{
function init_lsl_mu_updater2($plugin_file, $caller) {
if (!is_admin()) return null;
$plugin = plugin_basename($plugin_file);
$basename = strtolower( dirname($plugin) );
$filter_name = 'sl_updater_' . $basename;
$callback_name = 'sl_updater_' . str_replace( '-', '_', $basename );
if (!has_filter($filter_name))
add_filter($filter_name, array($caller, $callback_name), 10, 2);
init_lsl_mu_updater($plugin);
$args = apply_filters($filter_name, array(), array());
$plugin_updater = new lsl_sl_plugin_updater(isset($args['api_url']) ? $args['api_url'] : "" , $plugin_file, $args);
return $plugin_updater;
}
}
?>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment