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