1<?php 2/** 3 * Habari Update class 4 * 5 * Checks for updates to Habari and its libraries 6 * 7 * @access public 8 */ 9class Update extends Singleton 10{ 11 const UPDATE_URL = 'https://beacon.habariproject.org/'; 12 13 private $beacons = array(); 14 private $update; // SimpleXMLElement 15 16 /** 17 * Enables singleton working properly 18 * 19 * @see singleton.php 20 */ 21 protected static function instance() 22 { 23 return self::getInstanceOf( get_class() ); 24 } 25 26 /** 27 * Add a beaconid to the list of beaconids to version-check. 28 * 29 * @param string $name the name of the component that will be checked 30 * @param string $beaconid the id of the beacon to check 31 * @param string $current_version the current version of the resource represented by this beaconid 32 */ 33 public static function add( $name, $beaconid, $current_version ) 34 { 35 try { 36 if ( empty( $name ) || empty( $beaconid ) || empty( $current_version ) ) { 37 if ( empty( $name ) ) { 38 $name = "Unnamed plugin"; 39 } 40 // @locale Signifies the plugin or theme is missing needed information to check for an update 41 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) ) ); 42 } 43 44 self::instance()->beacons[ (string) $beaconid] = array( 'name' => (string) $name, 'version' => (string) $current_version ); 45 } 46 catch ( Exception $e ) { 47 // catch any exceptions generated by missing beacon information 48 EventLog::log( _t( 'Beacon updater failed! %s', array($e->getMessage())), 'err', 'update', 'habari' ); 49 50 // tell cron the check failed 51 return false; 52 } 53 } 54 55 /** 56 * Return true if the beacon data contains updates from the server 57 * 58 * @param array $beacon the beacon data from the $beacons array 59 * @return boolean true if there are updates available for this beacon 60 */ 61 private static function filter_unchanged( $beacon ) 62 { 63 return isset( $beacon['latest_version'] ); 64 } 65 66 67 /** 68 * Perform a check of all beaconids. 69 * Notifies update_check plugin hooks when checking so that they can add their beaconids to the list. 70 * @return array An array of update beacon information for components that have updates 71 * @throws Exception 72 */ 73 public static function check() 74 { 75 76 try { 77 78 // get a local version of the instance to save typing 79 $instance = self::instance(); 80 81 // load beacons 82 self::register_beacons(); 83 84 // setup the remote request 85 $request = new RemoteRequest( self::UPDATE_URL, 'POST' ); 86 87 // add all the beacon versions as parameters 88 $request->set_params( 89 array_map( 90 create_function( '$a', 'return $a["version"];' ), 91 $instance->beacons 92 ) 93 ); 94 // we're not desperate enough to wait too long 95 $request->set_timeout( 5 ); 96 97 // execute the request 98 $result = $request->execute(); 99 100 // grab the body of the response, which has our xml in it 101 $update_data = $request->get_response_body(); 102 103 // i don't know why we hold the XML in a class variable, but we'll keep doing that in this rewrite 104 $instance->update = new SimpleXMLElement( $update_data ); 105 106 foreach ( $instance->update as $beacon ) { 107 108 $beacon_id = (string)$beacon['id']; 109 $beacon_url = (string)$beacon['url']; 110 $beacon_type = isset( $beacon['type'] ) ? (string)$beacon['type'] : 'addon'; 111 112 // do we have this beacon? if not, don't process it 113 // even though we POST all our beacons to the update script right now, it still hands back the whole list 114 if ( empty( $instance->beacons[ $beacon_id ] ) ) { 115 continue; 116 } 117 118 // add the beacon's basic info 119 $instance->beacons[ $beacon_id ]['id'] = $beacon_id; 120 $instance->beacons[ $beacon_id ]['url'] = $beacon_url; 121 $instance->beacons[ $beacon_id ]['type'] = $beacon_type; 122 123 foreach ( $beacon->update as $update ) { 124 125 // pick out and cast all the values from the XML 126 $u = array( 127 'severity' => (string)$update['severity'], 128 'version' => (string)$update['version'], 129 'date' => isset( $update['date'] ) ? (string)$update['date'] : '', 130 'url' => isset( $update['url'] ) ? (string)$update['url'] : '', 131 'text' => (string)$update, 132 ); 133 134 135 // if the remote update info version is newer... we want all newer versions 136 if ( version_compare( $u['version'], $instance->beacons[ $beacon_id ]['version'] ) > 0 ) { 137 138 // if this version is more recent than all the other versions 139 if ( !isset( $instance->beacons[ $beacon_id ]['latest_version'] ) || version_compare( $u['version'], $instance->beacons[ $beacon_id ]['latest_version'] ) > 0 ) { 140 141 // set this as the latest version 142 $instance->beacons[ $beacon_id ]['latest_version'] = $u['version']; 143 144 } 145 146 // add the version to the list 147 $instance->beacons[ $beacon_id ]['updates'][ $u['version'] ] = $u; 148 149 } 150 151 } 152 153 } 154 155 // return an array of beacons that have updates 156 return array_filter( $instance->beacons, array( 'Update', 'filter_unchanged' ) ); 157 158 } 159 catch ( Exception $e ) { 160 // catches any RemoteRequest errors or XML parsing problems, etc. 161 // bubble up 162 throw $e; 163 } 164 165 } 166 167 /** 168 * Loop through all the active plugins and add their information to the list of plugins to check for updates. 169 */ 170 private static function add_plugins() 171 { 172 173 $plugins = Plugins::get_active(); 174 175 foreach ( $plugins as $plugin ) { 176 177 // name and version are required in the XML file, make sure GUID is set 178 if ( !isset( $plugin->info->guid ) ) { 179 continue; 180 } 181 182 Update::add( $plugin->info->name, $plugin->info->guid, $plugin->info->version ); 183 184 } 185 186 } 187 188 /** 189 * Endpoint for the update-check cronjob. 190 * Loads beacons, checks for updates from hp.o, and saves any updates to the DB. 191 * 192 * @param null $cronjob Unused. The CronJob object being executed when being run as cron. 193 * @return boolean True on successful check, false on any failure (so cron runs again). 194 */ 195 public static function cron( $cronjob = null ) 196 { 197 198 // register the beacons 199 self::register_beacons(); 200 201 // save the list of beacons we are using to check with 202 Options::set( 'updates_beacons', self::instance()->beacons ); 203 204 try { 205 // run the check 206 $updates = Update::check(); 207 208 // save the list of updates 209 Options::set( 'updates_available', $updates ); 210 211 EventLog::log( _t( 'Updates check CronJob completed successfully.' ), 'info', 'update', 'habari' ); 212 213 // return true, we succeeded 214 return true; 215 } 216 catch ( Exception $e ) { 217 // catch any exceptions generated by RemoteRequest or XML parsing 218 219 EventLog::log( _t( 'Updates check CronJob failed!' ), 'err', 'update', 'habari', $e->getMessage() ); 220 221 // tell cron the check failed 222 return false; 223 } 224 225 } 226 227 /** 228 * Register beacons to check for updates. 229 * Includes Habari core, all active plugins, and any pluggable that implements the update_check hook. 230 */ 231 private static function register_beacons() 232 { 233 234 // if there are already beacons, don't run again 235 if ( count( self::instance()->beacons ) > 0 ) { 236 return; 237 } 238 239 Update::add( 'Habari', '7a0313be-d8e3-11db-8314-0800200c9a66', Version::get_habariversion() ); 240 241 // add the active theme 242 self::add_theme(); 243 244 // add all active plugins 245 self::add_plugins(); 246 247 Plugins::act( 'update_check' ); 248 249 } 250 251 /** 252 * Add the currently active theme's information to the list of beacons to check for updates. 253 */ 254 private static function add_theme() 255 { 256 257 // get the active theme 258 $theme = Themes::get_active_data( true ); 259 260 // name and version are required in the XML file, make sure GUID is set 261 if ( isset( $theme['info']->guid ) ) { 262 Update::add( $theme['info']->name, $theme['info']->guid, $theme['info']->version ); 263 } 264 265 } 266 267 /** 268 * Compare the current set of plugins with those we last checked for updates. 269 * This is run by AdminHandler on every page load to make sure we always have fresh data on the dashboard. 270 */ 271 public static function check_plugins() 272 { 273 274 // register the beacons 275 self::register_beacons(); 276 277 // get the list we checked last time 278 $checked_list = Options::get( 'updates_beacons' ); 279 280 // if the lists are different 281 if ( $checked_list != self::instance()->beacons ) { 282 283 // remove any stored updates, just to avoid showing stale data 284 Options::delete( 'updates_available' ); 285 286 // schedule an update check the next time cron runs 287 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.' ) ); 288 289 } 290 291 } 292 293 /** 294 * Return all available updates, or the updates available for a single GUID. 295 * 296 * @param string $guid A GUID to return available updates for. 297 * @return array Array of all available updates if no GUID is specified. 298 * @return array A single GUID's updates, if GUID is specified and they are available. 299 * @return false If a single GUID is specified and there are no updates available for it. 300 */ 301 public static function updates_available( $guid = null ) 302 { 303 304 $updates = Options::get( 'updates_available', array() ); 305 306 if ( $guid == null ) { 307 return $updates; 308 } 309 else { 310 311 if ( isset( $updates[ $guid ] ) ) { 312 return $updates[ $guid ]; 313 } 314 else { 315 return false; 316 } 317 318 } 319 320 } 321 322} 323 324?> 325