<?php
/**
* Habari Update class
*
* Checks for updates to Habari and its libraries
*
* @access public
*/
class Update extends Singleton
{
const UPDATE_URL = 'https://beacon.habariproject.org/';
private $beacons = array();
private $update; // SimpleXMLElement
/**
* Enables singleton working properly
*
* @see singleton.php
*/
protected static function instance()
{
return self::getInstanceOf( get_class() );
}
/**
* Add a beaconid to the list of beaconids to version-check.
*
* @param string $name the name of the component that will be checked
* @param string $beaconid the id of the beacon to check
* @param string $current_version the current version of the resource represented by this beaconid
*/
public static function add( $name, $beaconid, $current_version )
{
try {
if ( empty( $name ) || empty( $beaconid ) || empty( $current_version ) ) {
if ( empty( $name ) ) {
$name = "Unnamed plugin";
}
// @locale Signifies the plugin or theme is missing needed information to check for an update
throw new Exception( _t( 'Invalid Beacon updater information added for %s. The plugin or theme cannot be identified. Check your plugin\'s or theme\'s XML.', array($name) ) );
}
self::instance()->beacons[ (string) $beaconid] = array( 'name' => (string) $name, 'version' => (string) $current_version );
}
catch ( Exception $e ) {
// catch any exceptions generated by missing beacon information
EventLog::log( _t( 'Beacon updater failed! %s', array($e->getMessage())), 'err', 'update', 'habari' );
// tell cron the check failed
return false;
}
}
/**
* Return true if the beacon data contains updates from the server
*
* @param array $beacon the beacon data from the $beacons array
* @return boolean true if there are updates available for this beacon
*/
private static function filter_unchanged( $beacon )
{
return isset( $beacon['latest_version'] );
}
/**
* Perform a check of all beaconids.
* Notifies update_check plugin hooks when checking so that they can add their beaconids to the list.
* @return array An array of update beacon information for components that have updates
* @throws Exception
*/
public static function check()
{
try {
// get a local version of the instance to save typing
$instance = self::instance();
// load beacons
self::register_beacons();
// setup the remote request
$request = new RemoteRequest( self::UPDATE_URL, 'POST' );
// add all the beacon versions as parameters
$request->set_params(
Utils::array_map_field($instance->beacons, 'version')
);
// we're not desperate enough to wait too long
$request->set_timeout( 5 );
// execute the request
$result = $request->execute();
// grab the body of the response, which has our xml in it
$update_data = $request->get_response_body();
// i don't know why we hold the XML in a class variable, but we'll keep doing that in this rewrite
$instance->update = new SimpleXMLElement( $update_data );
foreach ( $instance->update as $beacon ) {
$beacon_id = (string)$beacon['id'];
$beacon_url = (string)$beacon['url'];
$beacon_type = isset( $beacon['type'] ) ? (string)$beacon['type'] : 'addon';
// do we have this beacon? if not, don't process it
// even though we POST all our beacons to the update script right now, it still hands back the whole list
if ( empty( $instance->beacons[ $beacon_id ] ) ) {
continue;
}
// add the beacon's basic info
$instance->beacons[ $beacon_id ]['id'] = $beacon_id;
$instance->beacons[ $beacon_id ]['url'] = $beacon_url;
$instance->beacons[ $beacon_id ]['type'] = $beacon_type;
foreach ( $beacon->update as $update ) {
// pick out and cast all the values from the XML
$u = array(
'severity' => (string)$update['severity'],
'version' => (string)$update['version'],
'date' => isset( $update['date'] ) ? (string)$update['date'] : '',
'url' => isset( $update['url'] ) ? (string)$update['url'] : '',
'text' => (string)$update,
);
// if the remote update info version is newer... we want all newer versions
if ( version_compare( $u['version'], $instance->beacons[ $beacon_id ]['version'] ) > 0 ) {
// if this version is more recent than all the other versions
if ( !isset( $instance->beacons[ $beacon_id ]['latest_version'] ) || version_compare( $u['version'], $instance->beacons[ $beacon_id ]['latest_version'] ) > 0 ) {
// set this as the latest version
$instance->beacons[ $beacon_id ]['latest_version'] = $u['version'];
}
// add the version to the list
$instance->beacons[ $beacon_id ]['updates'][ $u['version'] ] = $u;
}
}
}
// return an array of beacons that have updates
return array_filter( $instance->beacons, array( 'Update', 'filter_unchanged' ) );
}
catch ( Exception $e ) {
// catches any RemoteRequest errors or XML parsing problems, etc.
// bubble up
throw $e;
}
}
/**
* Loop through all the active plugins and add their information to the list of plugins to check for updates.
*/
private static function add_plugins()
{
$plugins = Plugins::get_active();
foreach ( $plugins as $plugin ) {
// name and version are required in the XML file, make sure GUID is set
if ( !isset( $plugin->info->guid ) ) {
continue;
}
Update::add( $plugin->info->name, $plugin->info->guid, $plugin->info->version );
}
}
/**
* Endpoint for the update-check cronjob.
* Loads beacons, checks for updates from hp.o, and saves any updates to the DB.
*
* @param null $cronjob Unused. The CronJob object being executed when being run as cron.
* @return boolean True on successful check, false on any failure (so cron runs again).
*/
public static function cron( $cronjob = null )
{
// register the beacons
self::register_beacons();
// save the list of beacons we are using to check with
Options::set( 'updates_beacons', self::instance()->beacons );
try {
// run the check
$updates = Update::check();
// save the list of updates
Options::set( 'updates_available', $updates );
EventLog::log( _t( 'Updates check CronJob completed successfully.' ), 'info', 'update', 'habari' );
// return true, we succeeded
return true;
}
catch ( Exception $e ) {
// catch any exceptions generated by RemoteRequest or XML parsing
EventLog::log( _t( 'Updates check CronJob failed!' ), 'err', 'update', 'habari', $e->getMessage() );
// tell cron the check failed
return false;
}
}
/**
* Register beacons to check for updates.
* Includes Habari core, all active plugins, and any pluggable that implements the update_check hook.
*/
private static function register_beacons()
{
// if there are already beacons, don't run again
if ( count( self::instance()->beacons ) > 0 ) {
return;
}
Update::add( 'Habari', '7a0313be-d8e3-11db-8314-0800200c9a66', Version::get_habariversion() );
// add the active theme
self::add_theme();
// add all active plugins
self::add_plugins();
Plugins::act( 'update_check' );
}
/**
* Add the currently active theme's information to the list of beacons to check for updates.
*/
private static function add_theme()
{
// get the active theme
$theme = Themes::get_active_data( true );
// name and version are required in the XML file, make sure GUID is set
if ( isset( $theme['info']->guid ) ) {
Update::add( $theme['info']->name, $theme['info']->guid, $theme['info']->version );
}
}
/**
* Compare the current set of plugins with those we last checked for updates.
* This is run by AdminHandler on every page load to make sure we always have fresh data on the dashboard.
*/
public static function check_plugins()
{
// register the beacons
self::register_beacons();
// get the list we checked last time
$checked_list = Options::get( 'updates_beacons' );
// if the lists are different
if ( $checked_list != self::instance()->beacons ) {
// remove any stored updates, just to avoid showing stale data
Options::delete( 'updates_available' );
// schedule an update check the next time cron runs
CronTab::add_single_cron( 'update_check_single', array( 'Update', 'cron' ), HabariDateTime::date_create()->int, _t( 'Perform a single check for plugin updates, the plugin set has changed.' ) );
}
}
/**
* Return all available updates, or the updates available for a single GUID.
*
* @param string $guid A GUID to return available updates for.
* @return array Array of all available updates if no GUID is specified.
* @return array A single GUID's updates, if GUID is specified and they are available.
* @return false If a single GUID is specified and there are no updates available for it.
*/
public static function updates_available( $guid = null )
{
$updates = Options::get( 'updates_available', array() );
if ( $guid == null ) {
return $updates;
}
else {
if ( isset( $updates[ $guid ] ) ) {
return $updates[ $guid ];
}
else {
return false;
}
}
}
}
?>