Files
liceo-ariosto/administrator/components/com_installer/src/Model/UpdatesitesModel.php
2025-06-17 11:53:18 +02:00

662 lines
24 KiB
PHP

<?php
/**
* @package Joomla.Administrator
* @subpackage com_installer
*
* @copyright (C) 2014 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Component\Installer\Administrator\Model;
use Joomla\CMS\Factory;
use Joomla\CMS\Filesystem\Folder;
use Joomla\CMS\Installer\Installer;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\CMS\Object\CMSObject;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Table\UpdateSite as UpdateSiteTable;
use Joomla\Component\Installer\Administrator\Helper\InstallerHelper;
use Joomla\Database\ParameterType;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Installer Update Sites Model
*
* @since 3.4
*/
class UpdatesitesModel extends InstallerModel
{
/**
* Constructor.
*
* @param array $config An optional associative array of configuration settings.
* @param MVCFactoryInterface $factory The factory.
*
* @since 1.6
* @see \Joomla\CMS\MVC\Model\ListModel
*/
public function __construct($config = [], MVCFactoryInterface $factory = null)
{
if (empty($config['filter_fields'])) {
$config['filter_fields'] = [
'update_site_name',
'name',
'client_id',
'client',
'client_translated',
'status',
'type',
'type_translated',
'folder',
'folder_translated',
'update_site_id',
'enabled',
'supported',
];
}
parent::__construct($config, $factory);
}
/**
* Enable/Disable an extension.
*
* @param array $eid Extension ids to un/publish
* @param int $value Publish value
*
* @return boolean True on success
*
* @throws \Exception on ACL error
* @since 3.4
*
*/
public function publish(&$eid = [], $value = 1)
{
if (!$this->getCurrentUser()->authorise('core.edit.state', 'com_installer')) {
throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 403);
}
$result = true;
// Ensure eid is an array of extension ids
if (!\is_array($eid)) {
$eid = [$eid];
}
// Get a table object for the extension type
$table = new UpdateSiteTable($this->getDatabase());
// Enable the update site in the table and store it in the database
foreach ($eid as $i => $id) {
$table->load($id);
$table->enabled = $value;
if (!$table->store()) {
$this->setError($table->getError());
$result = false;
}
}
return $result;
}
/**
* Deletes an update site.
*
* @param array $ids Extension ids to delete.
*
* @return void
*
* @throws \Exception on ACL error
* @since 3.6
*
*/
public function delete($ids = [])
{
if (!$this->getCurrentUser()->authorise('core.delete', 'com_installer')) {
throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), 403);
}
// Ensure eid is an array of extension ids
if (!\is_array($ids)) {
$ids = [$ids];
}
$db = $this->getDatabase();
$app = Factory::getApplication();
$count = 0;
// Gets the update site names.
$query = $db->getQuery(true)
->select($db->quoteName(['update_site_id', 'name']))
->from($db->quoteName('#__update_sites'))
->whereIn($db->quoteName('update_site_id'), $ids);
$db->setQuery($query);
$updateSitesNames = $db->loadObjectList('update_site_id');
// Gets Joomla core update sites Ids.
$joomlaUpdateSitesIds = $this->getJoomlaUpdateSitesIds(0);
// Enable the update site in the table and store it in the database
foreach ($ids as $i => $id) {
// Don't allow to delete Joomla Core update sites.
if (\in_array((int) $id, $joomlaUpdateSitesIds)) {
$app->enqueueMessage(Text::sprintf('COM_INSTALLER_MSG_UPDATESITES_DELETE_CANNOT_DELETE', $updateSitesNames[$id]->name), 'error');
continue;
}
// Delete the update site from all tables.
try {
$id = (int) $id;
$query = $db->getQuery(true)
->delete($db->quoteName('#__update_sites'))
->where($db->quoteName('update_site_id') . ' = :id')
->bind(':id', $id, ParameterType::INTEGER);
$db->setQuery($query);
$db->execute();
$query = $db->getQuery(true)
->delete($db->quoteName('#__update_sites_extensions'))
->where($db->quoteName('update_site_id') . ' = :id')
->bind(':id', $id, ParameterType::INTEGER);
$db->setQuery($query);
$db->execute();
$query = $db->getQuery(true)
->delete($db->quoteName('#__updates'))
->where($db->quoteName('update_site_id') . ' = :id')
->bind(':id', $id, ParameterType::INTEGER);
$db->setQuery($query);
$db->execute();
$count++;
} catch (\RuntimeException $e) {
$app->enqueueMessage(
Text::sprintf(
'COM_INSTALLER_MSG_UPDATESITES_DELETE_ERROR',
$updateSitesNames[$id]->name,
$e->getMessage()
),
'error'
);
}
}
if ($count > 0) {
$app->enqueueMessage(Text::plural('COM_INSTALLER_MSG_UPDATESITES_N_DELETE_UPDATESITES_DELETED', $count), 'message');
}
}
/**
* Fetch the Joomla update sites ids.
*
* @param integer $column Column to return. 0 for update site ids, 1 for extension ids.
*
* @return array Array with joomla core update site ids.
*
* @since 3.6.0
*/
protected function getJoomlaUpdateSitesIds($column = 0)
{
$db = $this->getDatabase();
// Fetch the Joomla core update sites ids and their extension ids. We search for all except the core joomla extension with update sites.
$query = $db->getQuery(true)
->select($db->quoteName(['use.update_site_id', 'e.extension_id']))
->from($db->quoteName('#__update_sites_extensions', 'use'))
->join(
'LEFT',
$db->quoteName('#__update_sites', 'us'),
$db->quoteName('us.update_site_id') . ' = ' . $db->quoteName('use.update_site_id')
)
->join(
'LEFT',
$db->quoteName('#__extensions', 'e'),
$db->quoteName('e.extension_id') . ' = ' . $db->quoteName('use.extension_id')
)
->where('('
. '(' . $db->quoteName('e.type') . ' = ' . $db->quote('file') .
' AND ' . $db->quoteName('e.element') . ' = ' . $db->quote('joomla') . ')'
. ' OR (' . $db->quoteName('e.type') . ' = ' . $db->quote('package') . ' AND ' . $db->quoteName('e.element')
. ' = ' . $db->quote('pkg_en-GB') . ') OR (' . $db->quoteName('e.type') . ' = ' . $db->quote('component')
. ' AND ' . $db->quoteName('e.element') . ' = ' . $db->quote('com_joomlaupdate') . ')'
. ')');
$db->setQuery($query);
return $db->loadColumn($column);
}
/**
* Rebuild update sites tables.
*
* @return void
*
* @throws \Exception on ACL error
* @since 3.6
*
*/
public function rebuild(): void
{
if (!$this->getCurrentUser()->authorise('core.admin', 'com_installer')) {
throw new \Exception(Text::_('COM_INSTALLER_MSG_UPDATESITES_REBUILD_NOT_PERMITTED'), 403);
}
$db = $this->getDatabase();
$app = Factory::getApplication();
// Check if Joomla Extension plugin is enabled.
if (!PluginHelper::isEnabled('extension', 'joomla')) {
$query = $db->getQuery(true)
->select($db->quoteName('extension_id'))
->from($db->quoteName('#__extensions'))
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('element') . ' = ' . $db->quote('joomla'))
->where($db->quoteName('folder') . ' = ' . $db->quote('extension'));
$db->setQuery($query);
$pluginId = (int) $db->loadResult();
$link = Route::_('index.php?option=com_plugins&task=plugin.edit&extension_id=' . $pluginId);
$app->enqueueMessage(Text::sprintf('COM_INSTALLER_MSG_UPDATESITES_REBUILD_EXTENSION_PLUGIN_NOT_ENABLED', $link), 'error');
return;
}
$clients = [JPATH_SITE, JPATH_ADMINISTRATOR, JPATH_API];
$extensionGroupFolders = ['components', 'modules', 'plugins', 'templates', 'language', 'manifests'];
$pathsToSearch = [];
// Identifies which folders to search for manifest files.
foreach ($clients as $clientPath) {
foreach ($extensionGroupFolders as $extensionGroupFolderName) {
// Components, modules, plugins, templates, languages and manifest (files, libraries, etc)
if ($extensionGroupFolderName !== 'plugins') {
foreach (glob($clientPath . '/' . $extensionGroupFolderName . '/*', GLOB_NOSORT | GLOB_ONLYDIR) as $extensionFolderPath) {
$pathsToSearch[] = $extensionFolderPath;
}
} else {
// Plugins (another directory level is needed)
foreach (
glob(
$clientPath . '/' . $extensionGroupFolderName . '/*',
GLOB_NOSORT | GLOB_ONLYDIR
) as $pluginGroupFolderPath
) {
foreach (glob($pluginGroupFolderPath . '/*', GLOB_NOSORT | GLOB_ONLYDIR) as $extensionFolderPath) {
$pathsToSearch[] = $extensionFolderPath;
}
}
}
}
}
// Gets Joomla core update sites Ids.
$joomlaUpdateSitesIds = $this->getJoomlaUpdateSitesIds(0);
// First backup any custom extra_query for the sites
$query = $db->getQuery(true)
->select('TRIM(' . $db->quoteName('location') . ') AS ' . $db->quoteName('location') . ', ' . $db->quoteName('extra_query'))
->from($db->quoteName('#__update_sites'));
$db->setQuery($query);
$backupExtraQuerys = $db->loadAssocList('location');
// Delete from all tables (except joomla core update sites).
$query = $db->getQuery(true)
->delete($db->quoteName('#__update_sites'))
->whereNotIn($db->quoteName('update_site_id'), $joomlaUpdateSitesIds);
$db->setQuery($query);
$db->execute();
$query = $db->getQuery(true)
->delete($db->quoteName('#__update_sites_extensions'))
->whereNotIn($db->quoteName('update_site_id'), $joomlaUpdateSitesIds);
$db->setQuery($query);
$db->execute();
$query = $db->getQuery(true)
->delete($db->quoteName('#__updates'))
->whereNotIn($db->quoteName('update_site_id'), $joomlaUpdateSitesIds);
$db->setQuery($query);
$db->execute();
$count = 0;
// Gets Joomla core extension Ids.
$joomlaCoreExtensionIds = $this->getJoomlaUpdateSitesIds(1);
// Search for updateservers in manifest files inside the folders to search.
foreach ($pathsToSearch as $extensionFolderPath) {
$tmpInstaller = new Installer();
$tmpInstaller->setDatabase($this->getDatabase());
$tmpInstaller->setPath('source', $extensionFolderPath);
// Main folder manifests (higher priority)
$parentXmlfiles = Folder::files($tmpInstaller->getPath('source'), '.xml$', false, true);
// Search for children manifests (lower priority)
$allXmlFiles = Folder::files($tmpInstaller->getPath('source'), '.xml$', 1, true);
// Create a unique array of files ordered by priority
$xmlfiles = array_unique(array_merge($parentXmlfiles, $allXmlFiles));
if (!empty($xmlfiles)) {
foreach ($xmlfiles as $file) {
// Is it a valid Joomla installation manifest file?
$manifest = $tmpInstaller->isManifest($file);
if ($manifest !== null) {
/**
* Search if the extension exists in the extensions table. Excluding Joomla
* core extensions and discovered but not yet installed extensions.
*/
$name = (string) $manifest->name;
$pkgName = (string) $manifest->packagename;
$type = (string) $manifest['type'];
$query = $db->getQuery(true)
->select($db->quoteName('extension_id'))
->from($db->quoteName('#__extensions'))
->where(
[
$db->quoteName('type') . ' = :type',
$db->quoteName('state') . ' != -1',
]
)
->extendWhere(
'AND',
[
$db->quoteName('name') . ' = :name',
$db->quoteName('name') . ' = :pkgname',
],
'OR'
)
->whereNotIn($db->quoteName('extension_id'), $joomlaCoreExtensionIds)
->bind(':name', $name)
->bind(':pkgname', $pkgName)
->bind(':type', $type);
$db->setQuery($query);
$eid = (int) $db->loadResult();
if ($eid && $manifest->updateservers) {
// Set the manifest object and path
$tmpInstaller->manifest = $manifest;
$tmpInstaller->setPath('manifest', $file);
// Remove last extra_query as we are in a foreach
$tmpInstaller->extraQuery = '';
if (
$tmpInstaller->manifest->updateservers
&& $tmpInstaller->manifest->updateservers->server
&& isset($backupExtraQuerys[trim((string) $tmpInstaller->manifest->updateservers->server)])
) {
$tmpInstaller->extraQuery = $backupExtraQuerys[trim((string) $tmpInstaller->manifest->updateservers->server)]['extra_query'];
}
// Load the extension plugin (if not loaded yet).
PluginHelper::importPlugin('extension', 'joomla');
// Fire the onExtensionAfterUpdate
$app->triggerEvent('onExtensionAfterUpdate', ['installer' => $tmpInstaller, 'eid' => $eid]);
$count++;
}
}
}
}
}
if ($count > 0) {
$app->enqueueMessage(Text::_('COM_INSTALLER_MSG_UPDATESITES_REBUILD_SUCCESS'), 'message');
} else {
$app->enqueueMessage(Text::_('COM_INSTALLER_MSG_UPDATESITES_REBUILD_MESSAGE'), 'message');
}
// Flush the system cache to ensure extra_query is correctly loaded next time.
$this->cleanCache('_system');
}
/**
* Method to get an array of data items.
*
* @return mixed An array of data items on success, false on failure.
*
* @since 4.0.0
*/
public function getItems()
{
$items = parent::getItems();
array_walk(
$items,
static function ($item) {
$data = new CMSObject($item);
$item->downloadKey = InstallerHelper::getDownloadKey($data);
}
);
return $items;
}
/**
* Method to auto-populate the model state.
*
* Note. Calling getState in this method will result in recursion.
*
* @param string $ordering An optional ordering field.
* @param string $direction An optional direction (asc|desc).
*
* @return void
*
* @since 3.4
*/
protected function populateState($ordering = 'name', $direction = 'asc')
{
// Load the filter state.
$stateKeys = [
'search' => 'string',
'client_id' => 'int',
'enabled' => 'string',
'type' => 'string',
'folder' => 'string',
'supported' => 'int',
];
foreach ($stateKeys as $key => $filterType) {
$stateKey = 'filter.' . $key;
switch ($filterType) {
case 'int':
case 'bool':
$default = null;
break;
default:
$default = '';
break;
}
$stateValue = $this->getUserStateFromRequest(
$this->context . '.' . $stateKey,
'filter_' . $key,
$default,
$filterType
);
$this->setState($stateKey, $stateValue);
}
parent::populateState($ordering, $direction);
}
protected function getStoreId($id = '')
{
$id .= ':' . $this->getState('search');
$id .= ':' . $this->getState('client_id');
$id .= ':' . $this->getState('enabled');
$id .= ':' . $this->getState('type');
$id .= ':' . $this->getState('folder');
$id .= ':' . $this->getState('supported');
return parent::getStoreId($id);
}
/**
* Method to get the database query
*
* @return \Joomla\Database\DatabaseQuery The database query
*
* @since 3.4
*/
protected function getListQuery()
{
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select(
$db->quoteName(
[
's.update_site_id',
's.name',
's.type',
's.location',
's.enabled',
's.checked_out',
's.checked_out_time',
's.extra_query',
'e.extension_id',
'e.name',
'e.type',
'e.element',
'e.folder',
'e.client_id',
'e.state',
'e.manifest_cache',
'u.name',
],
[
'update_site_id',
'update_site_name',
'update_site_type',
'location',
'enabled',
'checked_out',
'checked_out_time',
'extra_query',
'extension_id',
'name',
'type',
'element',
'folder',
'client_id',
'state',
'manifest_cache',
'editor',
]
)
)
->from($db->quoteName('#__update_sites', 's'))
->join(
'INNER',
$db->quoteName('#__update_sites_extensions', 'se'),
$db->quoteName('se.update_site_id') . ' = ' . $db->quoteName('s.update_site_id')
)
->join(
'INNER',
$db->quoteName('#__extensions', 'e'),
$db->quoteName('e.extension_id') . ' = ' . $db->quoteName('se.extension_id')
)
->join(
'LEFT',
$db->quoteName('#__users', 'u'),
$db->quoteName('s.checked_out') . ' = ' . $db->quoteName('u.id')
)
->where($db->quoteName('state') . ' = 0');
// Process select filters.
$supported = $this->getState('filter.supported');
$enabled = $this->getState('filter.enabled');
$type = $this->getState('filter.type');
$clientId = $this->getState('filter.client_id');
$folder = $this->getState('filter.folder');
if ($enabled !== '') {
$enabled = (int) $enabled;
$query->where($db->quoteName('s.enabled') . ' = :enabled')
->bind(':enabled', $enabled, ParameterType::INTEGER);
}
if ($type) {
$query->where($db->quoteName('e.type') . ' = :type')
->bind(':type', $type);
}
if ($clientId !== null && $clientId !== '') {
$clientId = (int) $clientId;
$query->where($db->quoteName('e.client_id') . ' = :clientId')
->bind(':clientId', $clientId, ParameterType::INTEGER);
}
if ($folder !== '' && \in_array($type, ['plugin', 'library', ''], true)) {
$folderForBinding = $folder === '*' ? '' : $folder;
$query->where($db->quoteName('e.folder') . ' = :folder')
->bind(':folder', $folderForBinding);
}
// Process search filter (update site id).
$search = $this->getState('filter.search');
if (!empty($search) && stripos($search, 'id:') === 0) {
$uid = (int) substr($search, 3);
$query->where($db->quoteName('s.update_site_id') . ' = :siteId')
->bind(':siteId', $uid, ParameterType::INTEGER);
}
if (is_numeric($supported)) {
switch ($supported) {
// Show Update Sites which support Download Keys
case 1:
$supportedIDs = InstallerHelper::getDownloadKeySupportedSites($enabled);
break;
// Show Update Sites which are missing Download Keys
case -1:
$supportedIDs = InstallerHelper::getDownloadKeyExistsSites(false, $enabled);
break;
// Show Update Sites which have valid Download Keys
case 2:
$supportedIDs = InstallerHelper::getDownloadKeyExistsSites(true, $enabled);
break;
}
if (!empty($supportedIDs)) {
// Don't remove array_values(). whereIn expect a zero-based array.
$query->whereIn($db->quoteName('s.update_site_id'), array_values($supportedIDs));
} else {
// In case of an empty list of IDs we apply a fake filter to effectively return no data
$query->where($db->quoteName('s.update_site_id') . ' = 0');
}
}
/**
* Note: The search for name, ordering and pagination are processed by the parent InstallerModel class (in
* extension.php).
*/
return $query;
}
}