1 <?php
2 /**
3 * @package Joomla.Platform
4 * @subpackage Updater
5 *
6 * @copyright Copyright (C) 2005 - 2017 Open Source Matters, Inc. All rights reserved.
7 * @license GNU General Public License version 2 or later; see LICENSE
8 */
9
10 defined('JPATH_PLATFORM') or die;
11
12 jimport('joomla.filesystem.file');
13 jimport('joomla.filesystem.folder');
14 jimport('joomla.filesystem.path');
15 jimport('joomla.base.adapter');
16 jimport('joomla.utilities.arrayhelper');
17
18 /**
19 * Updater Class
20 *
21 * @since 11.1
22 */
23 class JUpdater extends JAdapter
24 {
25 /**
26 * Development snapshots, nightly builds, pre-release versions and so on
27 *
28 * @const integer
29 * @since 3.4
30 */
31 const STABILITY_DEV = 0;
32
33 /**
34 * Alpha versions (work in progress, things are likely to be broken)
35 *
36 * @const integer
37 * @since 3.4
38 */
39 const STABILITY_ALPHA = 1;
40
41 /**
42 * Beta versions (major functionality in place, show-stopper bugs are likely to be present)
43 *
44 * @const integer
45 * @since 3.4
46 */
47 const STABILITY_BETA = 2;
48
49 /**
50 * Release Candidate versions (almost stable, minor bugs might be present)
51 *
52 * @const integer
53 * @since 3.4
54 */
55 const STABILITY_RC = 3;
56
57 /**
58 * Stable versions (production quality code)
59 *
60 * @const integer
61 * @since 3.4
62 */
63 const STABILITY_STABLE = 4;
64
65 /**
66 * @var JUpdater JUpdater instance container.
67 * @since 11.3
68 */
69 protected static $instance;
70
71 /**
72 * Constructor
73 *
74 * @since 11.1
75 */
76 public function __construct()
77 {
78 // Adapter base path, class prefix
79 parent::__construct(__DIR__, 'JUpdater');
80 }
81
82 /**
83 * Returns a reference to the global Installer object, only creating it
84 * if it doesn't already exist.
85 *
86 * @return JUpdater An installer object
87 *
88 * @since 11.1
89 */
90 public static function getInstance()
91 {
92 if (!isset(self::$instance))
93 {
94 self::$instance = new JUpdater;
95 }
96
97 return self::$instance;
98 }
99
100 /**
101 * Finds the update for an extension. Any discovered updates are stored in the #__updates table.
102 *
103 * @param int|array $eid Extension Identifier or list of Extension Identifiers; if zero use all
104 * sites
105 * @param integer $cacheTimeout How many seconds to cache update information; if zero, force reload the
106 * update information
107 * @param integer $minimum_stability Minimum stability for the updates; 0=dev, 1=alpha, 2=beta, 3=rc,
108 * 4=stable
109 * @param boolean $includeCurrent Should I include the current version in the results?
110 *
111 * @return boolean True if there are updates
112 *
113 * @since 11.1
114 */
115 public function findUpdates($eid = 0, $cacheTimeout = 0, $minimum_stability = self::STABILITY_STABLE, $includeCurrent = false)
116 {
117 $retval = false;
118
119 $results = $this->getUpdateSites($eid);
120
121 if (empty($results))
122 {
123 return $retval;
124 }
125
126 $now = time();
127 $earliestTime = $now - $cacheTimeout;
128 $sitesWithUpdates = array();
129
130 if ($cacheTimeout > 0)
131 {
132 $sitesWithUpdates = $this->getSitesWithUpdates($earliestTime);
133 }
134
135 foreach ($results as $result)
136 {
137 /**
138 * If we have already checked for updates within the cache timeout period we will report updates available
139 * only if there are update records matching this update site. Then we skip processing of the update site
140 * since it's already processed within the cache timeout period.
141 */
142 if (($cacheTimeout > 0)
143 && isset($result['last_check_timestamp'])
144 && ($result['last_check_timestamp'] >= $earliestTime))
145 {
146 $retval = $retval || in_array($result['update_site_id'], $sitesWithUpdates);
147
148 continue;
149 }
150
151 $updateObjects = $this->getUpdateObjectsForSite($result, $minimum_stability, $includeCurrent);
152
153 if (!empty($updateObjects))
154 {
155 $retval = true;
156
157 /** @var JTableUpdate $update */
158 foreach ($updateObjects as $update)
159 {
160 $update->check();
161 $update->store();
162 }
163 }
164
165 // Finally, update the last update check timestamp
166 $this->updateLastCheckTimestamp($result['update_site_id']);
167 }
168
169 return $retval;
170 }
171
172 /**
173 * Finds an update for an extension
174 *
175 * @param integer $id Id of the extension
176 *
177 * @return mixed
178 *
179 * @since 3.6.0
180 *
181 * @deprecated 4.0 No replacement.
182 */
183 public function update($id)
184 {
185 $updaterow = JTable::getInstance('update');
186 $updaterow->load($id);
187 $update = new JUpdate;
188
189 if ($update->loadFromXml($updaterow->detailsurl))
190 {
191 return $update->install();
192 }
193
194 return false;
195 }
196
197 /**
198 * Returns the update site records for an extension with ID $eid. If $eid is zero all enabled update sites records
199 * will be returned.
200 *
201 * @param int $eid The extension ID to fetch.
202 *
203 * @return array
204 *
205 * @since 3.6.0
206 */
207 private function getUpdateSites($eid = 0)
208 {
209 $db = $this->getDbo();
210 $query = $db->getQuery(true);
211
212 $query->select('DISTINCT a.update_site_id, a.type, a.location, a.last_check_timestamp, a.extra_query')
213 ->from($db->quoteName('#__update_sites', 'a'))
214 ->where('a.enabled = 1');
215
216 if ($eid)
217 {
218 $query->join('INNER', '#__update_sites_extensions AS b ON a.update_site_id = b.update_site_id');
219
220 if (is_array($eid))
221 {
222 $query->where('b.extension_id IN (' . implode(',', $eid) . ')');
223 }
224 elseif ((int) $eid)
225 {
226 $query->where('b.extension_id = ' . $eid);
227 }
228 }
229
230 $db->setQuery($query);
231
232 $result = $db->loadAssocList();
233
234 if (!is_array($result))
235 {
236 return array();
237 }
238
239 return $result;
240 }
241
242 /**
243 * Loads the contents of an update site record $updateSite and returns the update objects
244 *
245 * @param array $updateSite The update site record to process
246 * @param int $minimum_stability Minimum stability for the returned update records
247 * @param bool $includeCurrent Should I also include the current version?
248 *
249 * @return array The update records. Empty array if no updates are found.
250 *
251 * @since 3.6.0
252 */
253 private function getUpdateObjectsForSite($updateSite, $minimum_stability = self::STABILITY_STABLE, $includeCurrent = false)
254 {
255 $retVal = array();
256
257 $this->setAdapter($updateSite['type']);
258
259 if (!isset($this->_adapters[$updateSite['type']]))
260 {
261 // Ignore update sites requiring adapters we don't have installed
262 return $retVal;
263 }
264
265 $updateSite['minimum_stability'] = $minimum_stability;
266
267 // Get the update information from the remote update XML document
268 /** @var JUpdateAdapter $adapter */
269 $adapter = $this->_adapters[ $updateSite['type']];
270 $update_result = $adapter->findUpdate($updateSite);
271
272 // Version comparison operator.
273 $operator = $includeCurrent ? 'ge' : 'gt';
274
275 if (is_array($update_result))
276 {
277 // If we have additional update sites in the remote (collection) update XML document, parse them
278 if (array_key_exists('update_sites', $update_result) && count($update_result['update_sites']))
279 {
280 $thisUrl = trim($updateSite['location']);
281 $thisId = (int) $updateSite['update_site_id'];
282
283 foreach ($update_result['update_sites'] as $extraUpdateSite)
284 {
285 $extraUrl = trim($extraUpdateSite['location']);
286 $extraId = (int) $extraUpdateSite['update_site_id'];
287
288 // Do not try to fetch the same update site twice
289 if (($thisId == $extraId) || ($thisUrl == $extraUrl))
290 {
291 continue;
292 }
293
294 $extraUpdates = $this->getUpdateObjectsForSite($extraUpdateSite, $minimum_stability);
295
296 if (count($extraUpdates))
297 {
298 $retVal = array_merge($retVal, $extraUpdates);
299 }
300 }
301 }
302
303 if (array_key_exists('updates', $update_result) && count($update_result['updates']))
304 {
305 /** @var JTableUpdate $current_update */
306 foreach ($update_result['updates'] as $current_update)
307 {
308 $current_update->extra_query = $updateSite['extra_query'];
309
310 /** @var JTableUpdate $update */
311 $update = JTable::getInstance('update');
312
313 /** @var JTableExtension $extension */
314 $extension = JTable::getInstance('extension');
315
316 $uid = $update
317 ->find(
318 array(
319 'element' => $current_update->get('element'),
320 'type' => $current_update->get('type'),
321 'client_id' => $current_update->get('client_id'),
322 'folder' => $current_update->get('folder'),
323 )
324 );
325
326 $eid = $extension
327 ->find(
328 array(
329 'element' => $current_update->get('element'),
330 'type' => $current_update->get('type'),
331 'client_id' => $current_update->get('client_id'),
332 'folder' => $current_update->get('folder'),
333 )
334 );
335
336 if (!$uid)
337 {
338 // Set the extension id
339 if ($eid)
340 {
341 // We have an installed extension, check the update is actually newer
342 $extension->load($eid);
343 $data = json_decode($extension->manifest_cache, true);
344
345 if (version_compare($current_update->version, $data['version'], $operator) == 1)
346 {
347 $current_update->extension_id = $eid;
348 $retVal[] = $current_update;
349 }
350 }
351 else
352 {
353 // A potentially new extension to be installed
354 $retVal[] = $current_update;
355 }
356 }
357 else
358 {
359 $update->load($uid);
360
361 // If there is an update, check that the version is newer then replaces
362 if (version_compare($current_update->version, $update->version, $operator) == 1)
363 {
364 $retVal[] = $current_update;
365 }
366 }
367 }
368 }
369 }
370
371 return $retVal;
372 }
373
374 /**
375 * Returns the IDs of the update sites with cached updates
376 *
377 * @param int $timestamp Optional. If set, only update sites checked before $timestamp will be taken into
378 * account.
379 *
380 * @return array The IDs of the update sites with cached updates
381 *
382 * @since 3.6.0
383 */
384 private function getSitesWithUpdates($timestamp = 0)
385 {
386 $db = JFactory::getDbo();
387
388 $query = $db->getQuery(true)
389 ->select('DISTINCT update_site_id')
390 ->from('#__updates');
391
392 if ($timestamp)
393 {
394 $subQuery = $db->getQuery(true)
395 ->select('update_site_id')
396 ->from('#__update_sites')
397 ->where($db->qn('last_check_timestamp') . ' IS NULL', 'OR')
398 ->where($db->qn('last_check_timestamp') . ' <= ' . $db->q($timestamp), 'OR');
399
400 $query->where($db->qn('update_site_id') . ' IN (' . $subQuery . ')');
401 }
402
403 $retVal = $db->setQuery($query)->loadColumn(0);
404
405 if (empty($retVal))
406 {
407 return array();
408 }
409
410 return $retVal;
411 }
412
413 /**
414 * Update the last check timestamp of an update site
415 *
416 * @param int $updateSiteId The update site ID to mark as just checked
417 *
418 * @return void
419 *
420 * @since 3.6.0
421 */
422 private function updateLastCheckTimestamp($updateSiteId)
423 {
424 $timestamp = time();
425 $db = JFactory::getDbo();
426
427 $query = $db->getQuery(true)
428 ->update($db->quoteName('#__update_sites'))
429 ->set($db->quoteName('last_check_timestamp') . ' = ' . $db->quote($timestamp))
430 ->where($db->quoteName('update_site_id') . ' = ' . $db->quote($updateSiteId));
431 $db->setQuery($query);
432 $db->execute();
433 }
434 }
435