cms/libraries/src/Updater/UpdateAdapter.php

291 lines
8.7 KiB
PHP

<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2008 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\CMS\Updater;
use Joomla\CMS\Adapter\AdapterInstance;
use Joomla\CMS\Factory;
use Joomla\CMS\Http\HttpFactory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Log\Log;
use Joomla\CMS\Version;
use Joomla\Database\ParameterType;
use Joomla\Registry\Registry;
// phpcs:disable PSR1.Files.SideEffects
\defined('JPATH_PLATFORM') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* UpdateAdapter class.
*
* @since 1.7.0
*/
abstract class UpdateAdapter extends AdapterInstance
{
/**
* Resource handle for the XML Parser
*
* @var resource
* @since 3.0.0
*/
protected $xmlParser;
/**
* Element call stack
*
* @var array
* @since 3.0.0
*/
protected $stack = ['base'];
/**
* ID of update site
*
* @var integer
* @since 3.0.0
*/
protected $updateSiteId = 0;
/**
* Columns in the extensions table to be updated
*
* @var array
* @since 3.0.0
*/
protected $updatecols = ['NAME', 'ELEMENT', 'TYPE', 'FOLDER', 'CLIENT', 'VERSION', 'DESCRIPTION', 'INFOURL', 'CHANGELOGURL', 'EXTRA_QUERY'];
/**
* Should we try appending a .xml extension to the update site's URL?
*
* @var boolean
*/
protected $appendExtension = false;
/**
* The name of the update site (used in logging)
*
* @var string
*/
protected $updateSiteName = '';
/**
* The update site URL from which we will get the update information
*
* @var string
*/
protected $_url = '';
/**
* The minimum stability required for updates to be taken into account. The possible values are:
* 0 dev Development snapshots, nightly builds, pre-release versions and so on
* 1 alpha Alpha versions (work in progress, things are likely to be broken)
* 2 beta Beta versions (major functionality in place, show-stopper bugs are likely to be present)
* 3 rc Release Candidate versions (almost stable, minor bugs might be present)
* 4 stable Stable versions (production quality code)
*
* @var integer
* @since 14.1
*
* @see Updater
*/
protected $minimum_stability = Updater::STABILITY_STABLE;
/**
* Gets the reference to the current direct parent
*
* @return string
*
* @since 1.7.0
*/
protected function _getStackLocation()
{
return implode('->', $this->stack);
}
/**
* Gets the reference to the last tag
*
* @return object
*
* @since 1.7.0
*/
protected function _getLastTag()
{
return $this->stack[\count($this->stack) - 1];
}
/**
* Finds an update
*
* @param array $options Options to use: update_site_id: the unique ID of the update site to look at
*
* @return array Update_sites and updates discovered
*
* @since 1.7.0
*/
abstract public function findUpdate($options);
/**
* Toggles the enabled status of an update site. Update sites are disabled before getting the update information
* from their URL and enabled afterwards. If the URL fetch fails with a PHP fatal error (e.g. timeout) the faulty
* update site will remain disabled the next time we attempt to load the update information.
*
* @param int $updateSiteId The numeric ID of the update site to enable/disable
* @param bool $enabled Enable the site when true, disable it when false
*
* @return void
*/
protected function toggleUpdateSite($updateSiteId, $enabled = true)
{
$updateSiteId = (int) $updateSiteId;
$enabled = (bool) $enabled ? 1 : 0;
if (empty($updateSiteId)) {
return;
}
$db = $this->parent->getDbo();
$query = $db->getQuery(true)
->update($db->quoteName('#__update_sites'))
->set($db->quoteName('enabled') . ' = :enabled')
->where($db->quoteName('update_site_id') . ' = :id')
->bind(':enabled', $enabled, ParameterType::INTEGER)
->bind(':id', $updateSiteId, ParameterType::INTEGER);
$db->setQuery($query);
try {
$db->execute();
} catch (\RuntimeException $e) {
// Do nothing
}
}
/**
* Get the name of an update site. This is used in logging.
*
* @param int $updateSiteId The numeric ID of the update site
*
* @return string The name of the update site or an empty string if it's not found
*/
protected function getUpdateSiteName($updateSiteId)
{
$updateSiteId = (int) $updateSiteId;
if (empty($updateSiteId)) {
return '';
}
$db = $this->parent->getDbo();
$query = $db->getQuery(true)
->select($db->quoteName('name'))
->from($db->quoteName('#__update_sites'))
->where($db->quoteName('update_site_id') . ' = :id')
->bind(':id', $updateSiteId, ParameterType::INTEGER);
$db->setQuery($query);
$name = '';
try {
$name = $db->loadResult();
} catch (\RuntimeException $e) {
// Do nothing
}
return $name;
}
/**
* Try to get the raw HTTP response from the update site, hopefully containing the update XML.
*
* @param array $options The update options, see findUpdate() in children classes
*
* @return \Joomla\CMS\Http\Response|bool False if we can't connect to the site, HTTP Response object otherwise
*
* @throws \Exception
*/
protected function getUpdateSiteResponse($options = [])
{
$url = trim($options['location']);
$this->_url = &$url;
$this->updateSiteId = $options['update_site_id'];
if (!isset($options['update_site_name'])) {
$options['update_site_name'] = $this->getUpdateSiteName($this->updateSiteId);
}
$this->updateSiteName = $options['update_site_name'];
$this->appendExtension = false;
if (\array_key_exists('append_extension', $options)) {
$this->appendExtension = $options['append_extension'];
}
if ($this->appendExtension && (substr($url, -4) !== '.xml')) {
if (substr($url, -1) !== '/') {
$url .= '/';
}
$url .= 'extension.xml';
}
// Disable the update site. If the get() below fails with a fatal error (e.g. timeout) the faulty update
// site will remain disabled
$this->toggleUpdateSite($this->updateSiteId, false);
$startTime = microtime(true);
$version = new Version();
$httpOption = new Registry();
$httpOption->set('userAgent', $version->getUserAgent('Joomla', true, false));
// JHttp transport throws an exception when there's no response.
try {
$http = HttpFactory::getHttp($httpOption);
$response = $http->get($url, [], 20);
} catch (\RuntimeException $e) {
$response = null;
}
// Enable the update site. Since the get() returned the update site should remain enabled
$this->toggleUpdateSite($this->updateSiteId, true);
// Log the time it took to load this update site's information
$endTime = microtime(true);
$timeToLoad = sprintf('%0.2f', $endTime - $startTime);
Log::add(
"Loading information from update site #{$this->updateSiteId} with name " .
"\"$this->updateSiteName\" and URL $url took $timeToLoad seconds",
Log::INFO,
'updater'
);
if ($response === null || $response->code !== 200) {
// If the URL is missing the .xml extension, try appending it and retry loading the update
if (!$this->appendExtension && (substr($url, -4) !== '.xml')) {
$options['append_extension'] = true;
return $this->getUpdateSiteResponse($options);
}
// Log the exact update site name and URL which could not be loaded
Log::add('Error opening url: ' . $url . ' for update site: ' . $this->updateSiteName, Log::WARNING, 'updater');
$app = Factory::getApplication();
$app->enqueueMessage(
html_entity_decode(Text::sprintf('JLIB_UPDATER_ERROR_OPEN_UPDATE_SITE', $this->updateSiteId, $this->updateSiteName, $url)),
'warning'
);
return false;
}
return $response;
}
}