first commit

This commit is contained in:
2025-06-17 11:53:18 +02:00
commit 9f0f7ba12b
8804 changed files with 1369176 additions and 0 deletions

View File

@ -0,0 +1,656 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_installer
*
* @copyright (C) 2011 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;
\defined('_JEXEC') or die;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\CMS\Schema\ChangeSet;
use Joomla\CMS\Table\Extension;
use Joomla\CMS\Version;
use Joomla\Component\Installer\Administrator\Helper\InstallerHelper;
use Joomla\Database\DatabaseQuery;
use Joomla\Database\Exception\ExecutionFailureException;
use Joomla\Database\ParameterType;
use Joomla\Registry\Registry;
\JLoader::register('JoomlaInstallerScript', JPATH_ADMINISTRATOR . '/components/com_admin/script.php');
/**
* Installer Database Model
*
* @since 1.6
*/
class DatabaseModel extends InstallerModel
{
/**
* Set the model context
*
* @var string
*
* @since 4.0.0
*/
protected $_context = 'com_installer.discover';
/**
* ChangeSet of all extensions
*
* @var array
*
* @since 4.0.0
*/
private $changeSetList = [];
/**
* Total of errors
*
* @var integer
*
* @since 4.0.0
*/
private $errorCount = 0;
/**
* Constructor.
*
* @param array $config An optional associative array of configuration settings.
* @param MVCFactoryInterface $factory The factory.
*
* @see ListModel
* @since 4.0.0
*/
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',
'extension_id',
];
}
parent::__construct($config, $factory);
}
/**
* Method to return the total number of errors in all the extensions, saved in cache.
*
* @return integer
*
* @throws \Exception
*
* @since 4.0.0
*/
public function getErrorCount()
{
return $this->errorCount;
}
/**
* Method to populate the schema cache.
*
* @param integer $cid The extension ID to get the schema for
*
* @return void
*
* @throws \Exception
*
* @since 4.0.0
*/
private function fetchSchemaCache($cid = 0)
{
// We already have it
if (\array_key_exists($cid, $this->changeSetList)) {
return;
}
// Add the ID to the state so it can be used for filtering
if ($cid) {
$this->setState('filter.extension_id', $cid);
}
// With the parent::save it can get the limit and we need to make sure it gets all extensions
$results = $this->_getList($this->getListQuery());
foreach ($results as $result) {
$errorMessages = [];
$errorCount = 0;
if (strcmp($result->element, 'joomla') === 0) {
$result->element = 'com_admin';
if (!$this->getDefaultTextFilters()) {
$errorMessages[] = Text::_('COM_INSTALLER_MSG_DATABASE_FILTER_ERROR');
$errorCount++;
}
}
$db = $this->getDatabase();
if ($result->type === 'component') {
$basePath = JPATH_ADMINISTRATOR . '/components/' . $result->element;
} elseif ($result->type === 'plugin') {
$basePath = JPATH_PLUGINS . '/' . $result->folder . '/' . $result->element;
} elseif ($result->type === 'module') {
// Typehint to integer to normalise some DBs returning strings and others integers
if ((int) $result->client_id === 1) {
$basePath = JPATH_ADMINISTRATOR . '/modules/' . $result->element;
} elseif ((int) $result->client_id === 0) {
$basePath = JPATH_SITE . '/modules/' . $result->element;
} else {
// Module with unknown client id!? - bail
continue;
}
} elseif ($result->type === 'file' && $result->element === 'com_admin') {
// Specific bodge for the Joomla CMS special database check which points to com_admin
$basePath = JPATH_ADMINISTRATOR . '/components/' . $result->element;
} else {
// Unknown extension type (library, files etc which don't have known SQL paths right now)
continue;
}
// Search the standard SQL Path for the SQL Updates and then if not there check the configuration of the XML
// file. This just gives us a small performance win of not parsing the XML every time.
$folderTmp = $basePath . '/sql/updates/';
if (!file_exists($folderTmp)) {
$installationXML = InstallerHelper::getInstallationXML(
$result->element,
$result->type,
$result->client_id,
$result->type === 'plugin' ? $result->folder : null
);
if ($installationXML !== null) {
$folderTmp = (string) $installationXML->update->schemas->schemapath[0];
$a = explode('/', $folderTmp);
array_pop($a);
$folderTmp = $basePath . '/' . implode('/', $a);
}
}
// Can't find the folder still - give up now and move on.
if (!file_exists($folderTmp)) {
continue;
}
$changeSet = new ChangeSet($db, $folderTmp);
// If the version in the #__schemas is different
// than the update files, add to problems message
$schema = $changeSet->getSchema();
// If the schema is empty we couldn't find any update files. Just ignore the extension.
if (empty($schema)) {
continue;
}
if ($result->version_id !== $schema) {
$errorMessages[] = Text::sprintf('COM_INSTALLER_MSG_DATABASE_SCHEMA_ERROR', $result->version_id, $schema);
$errorCount++;
}
// If the version in the manifest_cache is different than the
// version in the installation xml, add to problems message
$compareUpdateMessage = $this->compareUpdateVersion($result);
if ($compareUpdateMessage) {
$errorMessages[] = $compareUpdateMessage;
$errorCount++;
}
// If there are errors in the database, add to the problems message
$errors = $changeSet->check();
$errorsMessage = $this->getErrorsMessage($errors);
if ($errorsMessage) {
$errorMessages = array_merge($errorMessages, $errorsMessage);
$errorCount++;
}
// Number of database tables Checked and Skipped
$errorMessages = array_merge($errorMessages, $this->getOtherInformationMessage($changeSet->getStatus()));
// Set the total number of errors
$this->errorCount += $errorCount;
// Collect the extension details
$this->changeSetList[$result->extension_id] = [
'folderTmp' => $folderTmp,
'errorsMessage' => $errorMessages,
'errorsCount' => $errorCount,
'results' => $changeSet->getStatus(),
'schema' => $schema,
'extension' => $result,
];
}
}
/**
* 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 1.6
*/
protected function populateState($ordering = 'name', $direction = 'asc')
{
$this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string'));
$this->setState('filter.client_id', $this->getUserStateFromRequest($this->context . '.filter.client_id', 'filter_client_id', null, 'int'));
$this->setState('filter.type', $this->getUserStateFromRequest($this->context . '.filter.type', 'filter_type', '', 'string'));
$this->setState('filter.folder', $this->getUserStateFromRequest($this->context . '.filter.folder', 'filter_folder', '', 'string'));
parent::populateState($ordering, $direction);
}
/**
* Fixes database problems.
*
* @param array $cids List of the selected extensions to fix
*
* @return void|boolean
*
* @throws \Exception
*
* @since 4.0.0
*/
public function fix($cids = [])
{
$db = $this->getDatabase();
foreach ($cids as $i => $cid) {
// Load the database issues
$this->fetchSchemaCache($cid);
$changeSet = $this->changeSetList[$cid];
$changeSet['changeset'] = new ChangeSet($db, $changeSet['folderTmp']);
$changeSet['changeset']->fix();
$this->fixSchemaVersion($changeSet['changeset'], $changeSet['extension']->extension_id);
$this->fixUpdateVersion($changeSet['extension']->extension_id);
if ($changeSet['extension']->element === 'com_admin') {
$installer = new \JoomlaInstallerScript();
$installer->deleteUnexistingFiles();
$this->fixDefaultTextFilters();
}
}
}
/**
* Gets the changeset array.
*
* @return array Array with the information of the versions problems, errors and the extensions itself
*
* @throws \Exception
*
* @since 4.0.0
*/
public function getItems()
{
$this->fetchSchemaCache();
$results = parent::getItems();
$results = $this->mergeSchemaCache($results);
return $results;
}
/**
* Method to get the database query
*
* @return DatabaseQuery The database query
*
* @since 4.0.0
*/
protected function getListQuery()
{
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select(
$db->quoteName(
[
'extensions.client_id',
'extensions.element',
'extensions.extension_id',
'extensions.folder',
'extensions.manifest_cache',
'extensions.name',
'extensions.type',
'schemas.version_id',
]
)
)
->from(
$db->quoteName(
'#__schemas',
'schemas'
)
)
->join(
'INNER',
$db->quoteName('#__extensions', 'extensions'),
$db->quoteName('schemas.extension_id') . ' = ' . $db->quoteName('extensions.extension_id')
);
$type = $this->getState('filter.type');
$clientId = $this->getState('filter.client_id');
$extensionId = $this->getState('filter.extension_id');
$folder = $this->getState('filter.folder');
if ($type) {
$query->where($db->quoteName('extensions.type') . ' = :type')
->bind(':type', $type);
}
if ($clientId != '') {
$clientId = (int) $clientId;
$query->where($db->quoteName('extensions.client_id') . ' = :clientid')
->bind(':clientid', $clientId, ParameterType::INTEGER);
}
if ($extensionId != '') {
$extensionId = (int) $extensionId;
$query->where($db->quoteName('extensions.extension_id') . ' = :extensionid')
->bind(':extensionid', $extensionId, ParameterType::INTEGER);
}
if ($folder != '' && \in_array($type, ['plugin', 'library', ''])) {
$folder = $folder === '*' ? '' : $folder;
$query->where($db->quoteName('extensions.folder') . ' = :folder')
->bind(':folder', $folder);
}
// Process search filter (update site id).
$search = $this->getState('filter.search');
if (!empty($search) && stripos($search, 'id:') === 0) {
$ids = (int) substr($search, 3);
$query->where($db->quoteName('schemas.extension_id') . ' = :eid')
->bind(':eid', $ids, ParameterType::INTEGER);
}
return $query;
}
/**
* Merge the items that will be visible with the changeSet information in cache
*
* @param array $results extensions returned from parent::getItems().
*
* @return array the changeSetList of the merged items
*
* @since 4.0.0
*/
protected function mergeSchemaCache($results)
{
$changeSetList = $this->changeSetList;
$finalResults = [];
foreach ($results as $result) {
if (\array_key_exists($result->extension_id, $changeSetList) && $changeSetList[$result->extension_id]) {
$finalResults[] = $changeSetList[$result->extension_id];
}
}
return $finalResults;
}
/**
* Get version from #__schemas table.
*
* @param integer $extensionId id of the extensions.
*
* @return mixed the return value from the query, or null if the query fails.
*
* @throws \Exception
*
* @since 4.0.0
*/
public function getSchemaVersion($extensionId)
{
$db = $this->getDatabase();
$extensionId = (int) $extensionId;
$query = $db->getQuery(true)
->select($db->quoteName('version_id'))
->from($db->quoteName('#__schemas'))
->where($db->quoteName('extension_id') . ' = :extensionid')
->bind(':extensionid', $extensionId, ParameterType::INTEGER);
$db->setQuery($query);
return $db->loadResult();
}
/**
* Fix schema version if wrong.
*
* @param ChangeSet $changeSet Schema change set.
* @param integer $extensionId ID of the extensions.
*
* @return mixed string schema version if success, false if fail.
*
* @throws \Exception
*
* @since 4.0.0
*/
public function fixSchemaVersion($changeSet, $extensionId)
{
// Get correct schema version -- last file in array.
$schema = $changeSet->getSchema();
// Check value. If ok, don't do update.
if ($schema == $this->getSchemaVersion($extensionId)) {
return $schema;
}
// Delete old row.
$extensionId = (int) $extensionId;
$db = $this->getDatabase();
$query = $db->getQuery(true)
->delete($db->quoteName('#__schemas'))
->where($db->quoteName('extension_id') . ' = :extensionid')
->bind(':extensionid', $extensionId, ParameterType::INTEGER);
$db->setQuery($query)->execute();
// Add new row.
$query->clear()
->insert($db->quoteName('#__schemas'))
->columns($db->quoteName('extension_id') . ',' . $db->quoteName('version_id'))
->values(':extensionid, :schema')
->bind(':extensionid', $extensionId, ParameterType::INTEGER)
->bind(':schema', $schema);
$db->setQuery($query);
try {
$db->execute();
} catch (ExecutionFailureException $e) {
return false;
}
return $schema;
}
/**
* Get current version from #__extensions table.
*
* @param object $extension data from #__extensions of a single extension.
*
* @return mixed string message with the errors with the update version or null if none
*
* @since 4.0.0
*/
public function compareUpdateVersion($extension)
{
$updateVersion = json_decode($extension->manifest_cache)->version;
if ($extension->element === 'com_admin') {
$extensionVersion = JVERSION;
} else {
$installationXML = InstallerHelper::getInstallationXML(
$extension->element,
$extension->type,
$extension->client_id,
$extension->type === 'plugin' ? $extension->folder : null
);
$extensionVersion = (string) $installationXML->version;
}
if (version_compare($extensionVersion, $updateVersion) != 0) {
return Text::sprintf('COM_INSTALLER_MSG_DATABASE_UPDATEVERSION_ERROR', $updateVersion, $extension->name, $extensionVersion);
}
return null;
}
/**
* Get a message of the tables skipped and checked
*
* @param array $status status of of the update files
*
* @return array Messages with the errors with the update version
*
* @since 4.0.0
*/
private function getOtherInformationMessage($status)
{
$problemsMessage = [];
$problemsMessage[] = Text::sprintf('COM_INSTALLER_MSG_DATABASE_CHECKED_OK', \count($status['ok']));
$problemsMessage[] = Text::sprintf('COM_INSTALLER_MSG_DATABASE_SKIPPED', \count($status['skipped']));
return $problemsMessage;
}
/**
* Get a message with all errors found in a given extension
*
* @param array $errors data from #__extensions of a single extension.
*
* @return array List of messages with the errors in the database
*
* @since 4.0.0
*/
private function getErrorsMessage($errors)
{
$errorMessages = [];
foreach ($errors as $line => $error) {
$key = 'COM_INSTALLER_MSG_DATABASE_' . $error->queryType;
$messages = $error->msgElements;
$file = basename($error->file);
$message0 = $messages[0] ?? ' ';
$message1 = $messages[1] ?? ' ';
$message2 = $messages[2] ?? ' ';
$errorMessages[] = Text::sprintf($key, $file, $message0, $message1, $message2);
}
return $errorMessages;
}
/**
* Fix Joomla version in #__extensions table if wrong (doesn't equal \JVersion short version).
*
* @param integer $extensionId id of the extension
*
* @return mixed string update version if success, false if fail.
*
* @since 4.0.0
*/
public function fixUpdateVersion($extensionId)
{
$table = new Extension($this->getDatabase());
$table->load($extensionId);
$cache = new Registry($table->manifest_cache);
$updateVersion = $cache->get('version');
if ($table->get('type') === 'file' && $table->get('element') === 'joomla') {
$extensionVersion = new Version();
$extensionVersion = $extensionVersion->getShortVersion();
} else {
$installationXML = InstallerHelper::getInstallationXML(
$table->get('element'),
$table->get('type'),
$table->get('client_id'),
$table->get('type') === 'plugin' ? $table->get('folder') : null
);
$extensionVersion = (string) $installationXML->version;
}
if ($updateVersion === $extensionVersion) {
return $updateVersion;
}
$cache->set('version', $extensionVersion);
$table->set('manifest_cache', $cache->toString());
if ($table->store()) {
return $extensionVersion;
}
return false;
}
/**
* For version 2.5.x only
* Check if com_config parameters are blank.
*
* @return string default text filters (if any).
*
* @since 4.0.0
*/
public function getDefaultTextFilters()
{
$table = new Extension($this->getDatabase());
$table->load($table->find(['name' => 'com_config']));
return $table->params;
}
/**
* For version 2.5.x only
* Check if com_config parameters are blank. If so, populate with com_content text filters.
*
* @return void
*
* @since 4.0.0
*/
private function fixDefaultTextFilters()
{
$table = new Extension($this->getDatabase());
$table->load($table->find(['name' => 'com_config']));
// Check for empty $config and non-empty content filters.
if (!$table->params) {
// Get filters from com_content and store if you find them.
$contentParams = ComponentHelper::getComponent('com_content')->getParams();
if ($contentParams->get('filters')) {
$newParams = new Registry();
$newParams->set('filters', $contentParams->get('filters'));
$table->params = (string) $newParams;
$table->store();
}
}
}
}

View File

@ -0,0 +1,315 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_installer
*
* @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\Component\Installer\Administrator\Model;
use Joomla\CMS\Factory;
use Joomla\CMS\Installer\Installer;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\Database\DatabaseQuery;
use Joomla\Database\Exception\ExecutionFailureException;
use Joomla\Database\ParameterType;
use Joomla\Utilities\ArrayHelper;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Installer Discover Model
*
* @since 1.6
*/
class DiscoverModel extends InstallerModel
{
/**
* Constructor.
*
* @param array $config An optional associative array of configuration settings.
* @param MVCFactoryInterface $factory The factory.
*
* @see \Joomla\CMS\MVC\Model\ListModel
* @since 1.6
*/
public function __construct($config = [], MVCFactoryInterface $factory = null)
{
if (empty($config['filter_fields'])) {
$config['filter_fields'] = [
'name',
'client_id',
'client', 'client_translated',
'type', 'type_translated',
'folder', 'folder_translated',
'extension_id',
];
}
parent::__construct($config, $factory);
}
/**
* 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.1
*/
protected function populateState($ordering = 'name', $direction = 'asc')
{
$app = Factory::getApplication();
// Load the filter state.
$this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string'));
$this->setState('filter.client_id', $this->getUserStateFromRequest($this->context . '.filter.client_id', 'filter_client_id', null, 'int'));
$this->setState('filter.type', $this->getUserStateFromRequest($this->context . '.filter.type', 'filter_type', '', 'string'));
$this->setState('filter.folder', $this->getUserStateFromRequest($this->context . '.filter.folder', 'filter_folder', '', 'string'));
$this->setState('message', $app->getUserState('com_installer.message'));
$this->setState('extension_message', $app->getUserState('com_installer.extension_message'));
$app->setUserState('com_installer.message', '');
$app->setUserState('com_installer.extension_message', '');
parent::populateState($ordering, $direction);
}
/**
* Method to get the database query.
*
* @return DatabaseQuery The database query
*
* @since 3.1
*/
protected function getListQuery()
{
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__extensions'))
->where($db->quoteName('state') . ' = -1');
// Process select filters.
$type = $this->getState('filter.type');
$clientId = $this->getState('filter.client_id');
$folder = $this->getState('filter.folder');
if ($type) {
$query->where($db->quoteName('type') . ' = :type')
->bind(':type', $type);
}
if ($clientId != '') {
$clientId = (int) $clientId;
$query->where($db->quoteName('client_id') . ' = :clientid')
->bind(':clientid', $clientId, ParameterType::INTEGER);
}
if ($folder != '' && \in_array($type, ['plugin', 'library', ''])) {
$folder = $folder === '*' ? '' : $folder;
$query->where($db->quoteName('folder') . ' = :folder')
->bind(':folder', $folder);
}
// Process search filter.
$search = $this->getState('filter.search');
if (!empty($search)) {
if (stripos($search, 'id:') === 0) {
$ids = (int) substr($search, 3);
$query->where($db->quoteName('extension_id') . ' = :eid')
->bind(':eid', $ids, ParameterType::INTEGER);
}
}
// Note: The search for name, ordering and pagination are processed by the parent InstallerModel class (in extension.php).
return $query;
}
/**
* Discover extensions.
*
* Finds uninstalled extensions
*
* @return int The count of discovered extensions
*
* @since 1.6
*/
public function discover()
{
// Purge the list of discovered extensions and fetch them again.
$this->purge();
$results = Installer::getInstance()->discover();
// Get all templates, including discovered ones
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select($db->quoteName(['extension_id', 'element', 'folder', 'client_id', 'type']))
->from($db->quoteName('#__extensions'));
$db->setQuery($query);
$installedtmp = $db->loadObjectList();
$extensions = [];
foreach ($installedtmp as $install) {
$key = implode(
':',
[
$install->type,
str_replace('\\', '/', $install->element),
$install->folder,
$install->client_id,
]
);
$extensions[$key] = $install;
}
$count = 0;
foreach ($results as $result) {
// Check if we have a match on the element
$key = implode(
':',
[
$result->type,
str_replace('\\', '/', $result->element),
$result->folder,
$result->client_id,
]
);
if (!\array_key_exists($key, $extensions)) {
// Put it into the table
$result->check();
$result->store();
$count++;
}
}
return $count;
}
/**
* Installs a discovered extension.
*
* @return void
*
* @since 1.6
*/
public function discover_install()
{
$app = Factory::getApplication();
$input = $app->getInput();
$eid = $input->get('cid', 0, 'array');
if (\is_array($eid) || $eid) {
if (!\is_array($eid)) {
$eid = [$eid];
}
$eid = ArrayHelper::toInteger($eid);
$failed = false;
foreach ($eid as $id) {
$installer = new Installer();
$installer->setDatabase($this->getDatabase());
$result = $installer->discover_install($id);
if (!$result) {
$failed = true;
$app->enqueueMessage(Text::_('COM_INSTALLER_MSG_DISCOVER_INSTALLFAILED') . ': ' . $id);
}
}
// @todo - We are only receiving the message for the last Installer instance
$this->setState('action', 'remove');
$this->setState('name', $installer->get('name'));
$app->setUserState('com_installer.message', $installer->message);
$app->setUserState('com_installer.extension_message', $installer->get('extension_message'));
if (!$failed) {
$app->enqueueMessage(Text::_('COM_INSTALLER_MSG_DISCOVER_INSTALLSUCCESSFUL'), 'success');
}
} else {
$app->enqueueMessage(Text::_('COM_INSTALLER_MSG_DISCOVER_NOEXTENSIONSELECTED'));
}
}
/**
* Cleans out the list of discovered extensions.
*
* @return boolean True on success
*
* @since 1.6
*/
public function purge()
{
$db = $this->getDatabase();
$query = $db->getQuery(true)
->delete($db->quoteName('#__extensions'))
->where($db->quoteName('state') . ' = -1');
$db->setQuery($query);
try {
$db->execute();
} catch (ExecutionFailureException $e) {
$this->_message = Text::_('COM_INSTALLER_MSG_DISCOVER_FAILEDTOPURGEEXTENSIONS');
return false;
}
$this->_message = Text::_('COM_INSTALLER_MSG_DISCOVER_PURGEDDISCOVEREDEXTENSIONS');
return true;
}
/**
* Manipulate the query to be used to evaluate if this is an Empty State to provide specific conditions for this extension.
*
* @return DatabaseQuery
*
* @since 4.0.0
*/
protected function getEmptyStateQuery()
{
$query = parent::getEmptyStateQuery();
$query->where($this->getDatabase()->quoteName('state') . ' = -1');
return $query;
}
/**
* Checks for not installed extensions in extensions table.
*
* @return boolean True if there are discovered extensions in the database.
*
* @since 4.2.0
*/
public function checkExtensions()
{
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__extensions'))
->where($db->quoteName('state') . ' = -1');
$db->setQuery($query);
$discoveredExtensions = $db->loadObjectList();
return \count($discoveredExtensions) > 0;
}
}

View File

@ -0,0 +1,436 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_installer
*
* @copyright (C) 2006 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\Event\Installer\AfterInstallerEvent;
use Joomla\CMS\Event\Installer\BeforeInstallationEvent;
use Joomla\CMS\Event\Installer\BeforeInstallerEvent;
use Joomla\CMS\Factory;
use Joomla\CMS\Filesystem\File;
use Joomla\CMS\Installer\Installer;
use Joomla\CMS\Installer\InstallerHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Updater\Update;
use Joomla\CMS\Uri\Uri;
use Joomla\Filesystem\Path;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Extension Manager Install Model
*
* @since 1.5
*/
class InstallModel extends BaseDatabaseModel
{
/**
* @var \Joomla\CMS\Table\Table Table object
*/
protected $_table = null;
/**
* @var string URL
*/
protected $_url = null;
/**
* Model context string.
*
* @var string
*/
protected $_context = 'com_installer.install';
/**
* Method to auto-populate the model state.
*
* Note. Calling getState in this method will result in recursion.
*
* @return void
*
* @since 1.6
*/
protected function populateState()
{
$app = Factory::getApplication();
$this->setState('message', $app->getUserState('com_installer.message'));
$this->setState('extension_message', $app->getUserState('com_installer.extension_message'));
$app->setUserState('com_installer.message', '');
$app->setUserState('com_installer.extension_message', '');
parent::populateState();
}
/**
* Install an extension from either folder, URL or upload.
*
* @return boolean
*
* @since 1.5
*/
public function install()
{
$this->setState('action', 'install');
$app = Factory::getApplication();
$dispatcher = $this->getDispatcher();
// Load installer plugins for assistance if required:
PluginHelper::importPlugin('installer', null, true, $dispatcher);
$package = null;
// This event allows an input pre-treatment, a custom pre-packing or custom installation.
// (e.g. from a \JSON description).
$eventBefore = new BeforeInstallationEvent('onInstallerBeforeInstallation', [
'subject' => $this,
'package' => &$package, // @todo: Remove reference in Joomla 6, see InstallerEvent::__constructor()
]);
$results = $dispatcher->dispatch('onInstallerBeforeInstallation', $eventBefore)->getArgument('result', []);
$package = $eventBefore->getPackage();
if (\in_array(true, $results, true)) {
return true;
}
if (\in_array(false, $results, true)) {
return false;
}
$installType = $app->getInput()->getWord('installtype');
$installLang = $app->getInput()->getWord('package');
if ($package === null) {
switch ($installType) {
case 'folder':
// Remember the 'Install from Directory' path.
$app->getUserStateFromRequest($this->_context . '.install_directory', 'install_directory');
$package = $this->_getPackageFromFolder();
break;
case 'upload':
$package = $this->_getPackageFromUpload();
break;
case 'url':
$package = $this->_getPackageFromUrl();
break;
default:
$app->setUserState('com_installer.message', Text::_('COM_INSTALLER_NO_INSTALL_TYPE_FOUND'));
return false;
}
}
// No one of installType was able to resolve $package. Nothing to Install.
if (!$package) {
return false;
}
// This event allows a custom installation of the package or a customization of the package:
$eventBeforeInst = new BeforeInstallerEvent('onInstallerBeforeInstaller', [
'subject' => $this,
'package' => &$package, // @todo: Remove reference in Joomla 6, see InstallerEvent::__constructor()
]);
$results = $dispatcher->dispatch('onInstallerBeforeInstaller', $eventBeforeInst)->getArgument('result', []);
$package = $eventBeforeInst->getPackage();
if (\in_array(true, $results, true)) {
return true;
}
if (\in_array(false, $results, true)) {
if (\in_array($installType, ['upload', 'url'])) {
InstallerHelper::cleanupInstall($package['packagefile'], $package['extractdir']);
}
return false;
}
// Check if package was uploaded successfully.
if (!\is_array($package)) {
$app->enqueueMessage(Text::_('COM_INSTALLER_UNABLE_TO_FIND_INSTALL_PACKAGE'), 'error');
return false;
}
// Get an installer instance.
$installer = Installer::getInstance();
/*
* Check for a Joomla core package.
* To do this we need to set the source path to find the manifest (the same first step as Installer::install())
*
* This must be done before the unpacked check because InstallerHelper::detectType() returns a boolean false since the manifest
* can't be found in the expected location.
*/
if (isset($package['dir']) && is_dir($package['dir'])) {
$installer->setPath('source', $package['dir']);
if (!$installer->findManifest()) {
// If a manifest isn't found at the source, this may be a Joomla package; check the package directory for the Joomla manifest
if (file_exists($package['dir'] . '/administrator/manifests/files/joomla.xml')) {
// We have a Joomla package
if (\in_array($installType, ['upload', 'url'])) {
InstallerHelper::cleanupInstall($package['packagefile'], $package['extractdir']);
}
$app->enqueueMessage(
Text::sprintf('COM_INSTALLER_UNABLE_TO_INSTALL_JOOMLA_PACKAGE', Route::_('index.php?option=com_joomlaupdate')),
'warning'
);
return false;
}
}
}
// Was the package unpacked?
if (empty($package['type'])) {
if (\in_array($installType, ['upload', 'url'])) {
InstallerHelper::cleanupInstall($package['packagefile'], $package['extractdir']);
}
$app->enqueueMessage(Text::_('JLIB_INSTALLER_ABORT_DETECTMANIFEST'), 'error');
return false;
}
// Install the package.
if (!$installer->install($package['dir'])) {
// There was an error installing the package.
$msg = Text::sprintf('COM_INSTALLER_INSTALL_ERROR', Text::_('COM_INSTALLER_TYPE_TYPE_' . strtoupper($package['type'])));
$result = false;
$msgType = 'error';
} else {
// Package installed successfully.
$msg = Text::sprintf('COM_INSTALLER_INSTALL_SUCCESS', Text::_('COM_INSTALLER_TYPE_TYPE_' . strtoupper($installLang . $package['type'])));
$result = true;
$msgType = 'message';
}
// This event allows a custom a post-flight:
$eventAfterInst = new AfterInstallerEvent('onInstallerAfterInstaller', [
'subject' => $this,
'package' => &$package, // @todo: Remove reference in Joomla 6, see InstallerEvent::__constructor()
'installer' => $installer,
'installerResult' => &$result, // @todo: Remove reference in Joomla 6, see AfterInstallerEvent::__constructor()
'message' => &$msg, // @todo: Remove reference in Joomla 6, see AfterInstallerEvent::__constructor()
]);
$dispatcher->dispatch('onInstallerAfterInstaller', $eventAfterInst);
$package = $eventAfterInst->getPackage();
$result = $eventAfterInst->getInstallerResult();
$msg = $eventAfterInst->getMessage();
// Set some model state values.
$app->enqueueMessage($msg, $msgType);
$this->setState('name', $installer->get('name'));
$this->setState('result', $result);
$app->setUserState('com_installer.message', $installer->message);
$app->setUserState('com_installer.extension_message', $installer->get('extension_message'));
$app->setUserState('com_installer.redirect_url', $installer->get('redirect_url'));
// Cleanup the install files.
if (!is_file($package['packagefile'])) {
$package['packagefile'] = $app->get('tmp_path') . '/' . $package['packagefile'];
}
InstallerHelper::cleanupInstall($package['packagefile'], $package['extractdir']);
// Clear the cached extension data and menu cache
$this->cleanCache('_system');
$this->cleanCache('com_modules');
$this->cleanCache('com_plugins');
$this->cleanCache('mod_menu');
return $result;
}
/**
* Works out an installation package from a HTTP upload.
*
* @return mixed Package definition or false on failure.
*/
protected function _getPackageFromUpload()
{
// Get the uploaded file information.
$input = Factory::getApplication()->getInput();
// Do not change the filter type 'raw'. We need this to let files containing PHP code to upload. See \JInputFiles::get.
$userfile = $input->files->get('install_package', null, 'raw');
// Make sure that file uploads are enabled in php.
if (!(bool) ini_get('file_uploads')) {
Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLFILE'), 'error');
return false;
}
// Make sure that zlib is loaded so that the package can be unpacked.
if (!\extension_loaded('zlib')) {
Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLZLIB'), 'error');
return false;
}
// If there is no uploaded file, we have a problem...
if (!\is_array($userfile)) {
Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_NO_FILE_SELECTED'), 'error');
return false;
}
// Is the PHP tmp directory missing?
if ($userfile['error'] && ($userfile['error'] == UPLOAD_ERR_NO_TMP_DIR)) {
Factory::getApplication()->enqueueMessage(
Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR') . '<br>' . Text::_('COM_INSTALLER_MSG_WARNINGS_PHPUPLOADNOTSET'),
'error'
);
return false;
}
// Is the max upload size too small in php.ini?
if ($userfile['error'] && ($userfile['error'] == UPLOAD_ERR_INI_SIZE)) {
Factory::getApplication()->enqueueMessage(
Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR') . '<br>' . Text::_('COM_INSTALLER_MSG_WARNINGS_SMALLUPLOADSIZE'),
'error'
);
return false;
}
// Check if there was a different problem uploading the file.
if ($userfile['error'] || $userfile['size'] < 1) {
Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR'), 'error');
return false;
}
// Build the appropriate paths.
$config = Factory::getApplication()->getConfig();
$tmp_dest = $config->get('tmp_path') . '/' . $userfile['name'];
$tmp_src = $userfile['tmp_name'];
// Move uploaded file.
File::upload($tmp_src, $tmp_dest, false, true);
// Unpack the downloaded package file.
$package = InstallerHelper::unpack($tmp_dest, true);
return $package;
}
/**
* Install an extension from a directory
*
* @return array Package details or false on failure
*
* @since 1.5
*/
protected function _getPackageFromFolder()
{
$input = Factory::getApplication()->getInput();
// Get the path to the package to install.
$p_dir = $input->getString('install_directory');
$p_dir = Path::clean($p_dir);
// Did you give us a valid directory?
if (!is_dir($p_dir)) {
Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_PLEASE_ENTER_A_PACKAGE_DIRECTORY'), 'error');
return false;
}
// Detect the package type
$type = InstallerHelper::detectType($p_dir);
// Did you give us a valid package?
if (!$type) {
Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_PATH_DOES_NOT_HAVE_A_VALID_PACKAGE'), 'error');
}
$package['packagefile'] = null;
$package['extractdir'] = null;
$package['dir'] = $p_dir;
$package['type'] = $type;
return $package;
}
/**
* Install an extension from a URL.
*
* @return bool|array Package details or false on failure.
*
* @since 1.5
*/
protected function _getPackageFromUrl()
{
$input = Factory::getApplication()->getInput();
// Get the URL of the package to install.
$url = $input->getString('install_url');
// Did you give us a URL?
if (!$url) {
Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_ENTER_A_URL'), 'error');
return false;
}
// We only allow http & https here
$uri = new Uri($url);
if (!\in_array($uri->getScheme(), ['http', 'https'])) {
Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_INVALID_URL_SCHEME'), 'error');
return false;
}
// Handle updater XML file case:
if (preg_match('/\.xml\s*$/', $url)) {
$update = new Update();
$update->loadFromXml($url);
$package_url = trim($update->get('downloadurl', false)->_data);
if ($package_url) {
$url = $package_url;
}
unset($update);
}
// Download the package at the URL given.
$p_file = InstallerHelper::downloadPackage($url);
// Was the package downloaded?
if (!$p_file) {
Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_INVALID_URL'), 'error');
return false;
}
$tmp_dest = Factory::getApplication()->get('tmp_path');
// Unpack the downloaded package file.
$package = InstallerHelper::unpack($tmp_dest . '/' . $p_file, true);
return $package;
}
}

View File

@ -0,0 +1,223 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_installer
*
* @copyright (C) 2006 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\Language\Text;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\CMS\MVC\Model\ListModel;
use Joomla\Database\DatabaseQuery;
use Joomla\Utilities\ArrayHelper;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Extension Manager Abstract Extension Model.
*
* @since 1.5
*/
class InstallerModel extends ListModel
{
/**
* Constructor.
*
* @param array $config An optional associative array of configuration settings.
* @param MVCFactoryInterface $factory The factory.
*
* @see \Joomla\CMS\MVC\Model\ListModel
* @since 1.6
*/
public function __construct($config = [], MVCFactoryInterface $factory = null)
{
if (empty($config['filter_fields'])) {
$config['filter_fields'] = [
'name',
'client_id',
'client', 'client_translated',
'enabled',
'type', 'type_translated',
'folder', 'folder_translated',
'extension_id',
'creationDate',
];
}
parent::__construct($config, $factory);
}
/**
* Returns an object list
*
* @param DatabaseQuery $query The query
* @param int $limitstart Offset
* @param int $limit The number of records
*
* @return object[]
*/
protected function _getList($query, $limitstart = 0, $limit = 0)
{
$listOrder = $this->getState('list.ordering', 'name');
$listDirn = $this->getState('list.direction', 'asc');
// Replace slashes so preg_match will work
$search = $this->getState('filter.search');
$search = str_replace('/', ' ', $search);
$db = $this->getDatabase();
// Define which fields have to be processed in a custom way because of translation.
$customOrderFields = ['name', 'client_translated', 'type_translated', 'folder_translated', 'creationDate'];
// Process searching, ordering and pagination for fields that need to be translated.
if (\in_array($listOrder, $customOrderFields) || (!empty($search) && stripos($search, 'id:') !== 0)) {
// Get results from database and translate them.
$db->setQuery($query);
$result = $db->loadObjectList();
$this->translate($result);
// Process searching.
if (!empty($search) && stripos($search, 'id:') !== 0) {
$escapedSearchString = $this->refineSearchStringToRegex($search, '/');
// By default search only the extension name field.
$searchFields = ['name'];
// If in update sites view search also in the update site name field.
if ($this instanceof UpdatesitesModel) {
$searchFields[] = 'update_site_name';
}
foreach ($result as $i => $item) {
// Check if search string exists in any of the fields to be searched.
$found = 0;
foreach ($searchFields as $key => $field) {
if (!$found && preg_match('/' . $escapedSearchString . '/i', $item->{$field})) {
$found = 1;
}
}
// If search string was not found in any of the fields searched remove it from results array.
if (!$found) {
unset($result[$i]);
}
}
}
// Process ordering.
// Sort array object by selected ordering and selected direction. Sort is case insensitive and using locale sorting.
$result = ArrayHelper::sortObjects($result, $listOrder, strtolower($listDirn) == 'desc' ? -1 : 1, false, true);
// Process pagination.
$total = \count($result);
$this->cache[$this->getStoreId('getTotal')] = $total;
if ($total <= $limitstart) {
$limitstart = 0;
$this->setState('list.limitstart', 0);
}
return \array_slice($result, $limitstart, $limit ?: null);
}
// Process searching, ordering and pagination for regular database fields.
$query->order($db->quoteName($listOrder) . ' ' . $db->escape($listDirn));
$result = parent::_getList($query, $limitstart, $limit);
$this->translate($result);
return $result;
}
/**
* Translate a list of objects
*
* @param array $items The array of objects
*
* @return void
*/
protected function translate(&$items)
{
$lang = Factory::getLanguage();
foreach ($items as &$item) {
if (\strlen($item->manifest_cache) && $data = json_decode($item->manifest_cache)) {
foreach ($data as $key => $value) {
if ($key == 'type') {
// Ignore the type field
continue;
}
$item->$key = $value;
}
}
$item->author_info = @$item->authorEmail . '<br>' . @$item->authorUrl;
$item->client = Text::_([0 => 'JSITE', 1 => 'JADMINISTRATOR', 3 => 'JAPI'][$item->client_id] ?? 'JSITE');
$item->client_translated = $item->client;
$item->type_translated = Text::_('COM_INSTALLER_TYPE_' . strtoupper($item->type));
$item->folder_translated = @$item->folder ? $item->folder : Text::_('COM_INSTALLER_TYPE_NONAPPLICABLE');
$path = $item->client_id ? JPATH_ADMINISTRATOR : JPATH_SITE;
switch ($item->type) {
case 'component':
$extension = $item->element;
$source = JPATH_ADMINISTRATOR . '/components/' . $extension;
$lang->load("$extension.sys", JPATH_ADMINISTRATOR) || $lang->load("$extension.sys", $source);
break;
case 'file':
$extension = 'files_' . $item->element;
$lang->load("$extension.sys", JPATH_SITE);
break;
case 'library':
$parts = explode('/', $item->element);
$vendor = (isset($parts[1]) ? $parts[0] : null);
$extension = 'lib_' . ($vendor ? implode('_', $parts) : $item->element);
if (!$lang->load("$extension.sys", $path)) {
$source = $path . '/libraries/' . ($vendor ? $vendor . '/' . $parts[1] : $item->element);
$lang->load("$extension.sys", $source);
}
break;
case 'module':
$extension = $item->element;
$source = $path . '/modules/' . $extension;
$lang->load("$extension.sys", $path) || $lang->load("$extension.sys", $source);
break;
case 'plugin':
$extension = 'plg_' . $item->folder . '_' . $item->element;
$source = JPATH_PLUGINS . '/' . $item->folder . '/' . $item->element;
$lang->load("$extension.sys", JPATH_ADMINISTRATOR) || $lang->load("$extension.sys", $source);
break;
case 'template':
$extension = 'tpl_' . $item->element;
$source = $path . '/templates/' . $item->element;
$lang->load("$extension.sys", $path) || $lang->load("$extension.sys", $source);
break;
case 'package':
default:
$extension = $item->element;
$lang->load("$extension.sys", JPATH_SITE);
break;
}
// Translate the extension name if possible
$item->name = Text::_($item->name);
settype($item->description, 'string');
if (!\in_array($item->type, ['language'])) {
$item->description = Text::_($item->description);
}
}
}
}

View File

@ -0,0 +1,269 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_installer
* @copyright (C) 2006 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\Http\HttpFactory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\CMS\MVC\Model\ListModel;
use Joomla\String\StringHelper;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Languages Installer Model
*
* @since 2.5.7
*/
class LanguagesModel extends ListModel
{
/**
* Language count
*
* @var integer
* @since 3.7.0
*/
private $languageCount;
/**
* Constructor.
*
* @param array $config An optional associative array of configuration settings.
* @param MVCFactoryInterface $factory The factory.
*
* @see \Joomla\CMS\MVC\Model\ListModel
* @since 1.6
*/
public function __construct($config = [], MVCFactoryInterface $factory = null)
{
if (empty($config['filter_fields'])) {
$config['filter_fields'] = [
'name',
'element',
];
}
parent::__construct($config, $factory);
}
/**
* Get the Update Site
*
* @since 3.7.0
*
* @return string The URL of the Accredited Languagepack Updatesite XML
*/
private function getUpdateSite()
{
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select($db->quoteName('us.location'))
->from($db->quoteName('#__extensions', 'e'))
->where($db->quoteName('e.type') . ' = ' . $db->quote('package'))
->where($db->quoteName('e.element') . ' = ' . $db->quote('pkg_en-GB'))
->where($db->quoteName('e.client_id') . ' = 0')
->join(
'LEFT',
$db->quoteName('#__update_sites_extensions', 'use')
. ' ON ' . $db->quoteName('use.extension_id') . ' = ' . $db->quoteName('e.extension_id')
)
->join(
'LEFT',
$db->quoteName('#__update_sites', 'us')
. ' ON ' . $db->quoteName('us.update_site_id') . ' = ' . $db->quoteName('use.update_site_id')
);
return $db->setQuery($query)->loadResult();
}
/**
* Method to get an array of data items.
*
* @return mixed An array of data items on success, false on failure.
*
* @since 3.7.0
*/
public function getItems()
{
// Get a storage key.
$store = $this->getStoreId();
// Try to load the data from internal storage.
if (isset($this->cache[$store])) {
return $this->cache[$store];
}
try {
// Load the list items and add the items to the internal cache.
$this->cache[$store] = $this->getLanguages();
} catch (\RuntimeException $e) {
$this->setError($e->getMessage());
return false;
}
return $this->cache[$store];
}
/**
* Gets an array of objects from the updatesite.
*
* @return object[] An array of results.
*
* @since 3.0
* @throws \RuntimeException
*/
protected function getLanguages()
{
$updateSite = $this->getUpdateSite();
// Check whether the updateserver is found
if (empty($updateSite)) {
Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_WARNING_NO_LANGUAGES_UPDATESERVER'), 'warning');
return;
}
try {
$response = HttpFactory::getHttp()->get($updateSite);
} catch (\RuntimeException $e) {
$response = null;
}
if ($response === null || $response->code !== 200) {
Factory::getApplication()->enqueueMessage(Text::sprintf('COM_INSTALLER_MSG_ERROR_CANT_CONNECT_TO_UPDATESERVER', $updateSite), 'error');
return;
}
$updateSiteXML = simplexml_load_string($response->body);
if (!$updateSiteXML) {
Factory::getApplication()->enqueueMessage(Text::sprintf('COM_INSTALLER_MSG_ERROR_CANT_RETRIEVE_XML', $updateSite), 'error');
return;
}
$languages = [];
$search = strtolower($this->getState('filter.search'));
foreach ($updateSiteXML->extension as $extension) {
$language = new \stdClass();
foreach ($extension->attributes() as $key => $value) {
$language->$key = (string) $value;
}
if ($search) {
if (
strpos(strtolower($language->name), $search) === false
&& strpos(strtolower($language->element), $search) === false
) {
continue;
}
}
$languages[$language->name] = $language;
}
// Workaround for php 5.3
$that = $this;
// Sort the array by value of subarray
usort(
$languages,
function ($a, $b) use ($that) {
$ordering = $that->getState('list.ordering');
if (strtolower($that->getState('list.direction')) === 'asc') {
return StringHelper::strcmp($a->$ordering, $b->$ordering);
}
return StringHelper::strcmp($b->$ordering, $a->$ordering);
}
);
// Count the non-paginated list
$this->languageCount = \count($languages);
$limit = ($this->getState('list.limit') > 0) ? $this->getState('list.limit') : $this->languageCount;
return \array_slice($languages, $this->getStart(), $limit);
}
/**
* Returns a record count for the updatesite.
*
* @param \Joomla\Database\DatabaseQuery|string $query The query.
*
* @return integer Number of rows for query.
*
* @since 3.7.0
*/
protected function _getListCount($query)
{
return $this->languageCount;
}
/**
* Method to get a store id based on model configuration state.
*
* @param string $id A prefix for the store id.
*
* @return string A store id.
*
* @since 2.5.7
*/
protected function getStoreId($id = '')
{
// Compile the store id.
$id .= ':' . $this->getState('filter.search');
return parent::getStoreId($id);
}
/**
* Method to auto-populate the model state.
*
* Note. Calling getState in this method will result in recursion.
*
* @param string $ordering list order
* @param string $direction direction in the list
*
* @return void
*
* @since 2.5.7
*/
protected function populateState($ordering = 'name', $direction = 'asc')
{
$this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string'));
$this->setState('extension_message', Factory::getApplication()->getUserState('com_installer.extension_message'));
parent::populateState($ordering, $direction);
}
/**
* Method to compare two languages in order to sort them.
*
* @param object $lang1 The first language.
* @param object $lang2 The second language.
*
* @return integer
*
* @since 3.7.0
*/
protected function compareLanguages($lang1, $lang2)
{
return strcmp($lang1->name, $lang2->name);
}
}

View File

@ -0,0 +1,466 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_installer
*
* @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\Component\Installer\Administrator\Model;
use Joomla\CMS\Changelog\Changelog;
use Joomla\CMS\Event\Model\BeforeChangeStateEvent;
use Joomla\CMS\Extension\ExtensionHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\Installer\Installer;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Layout\FileLayout;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\Table\Extension;
use Joomla\Component\Templates\Administrator\Table\StyleTable;
use Joomla\Database\DatabaseQuery;
use Joomla\Database\ParameterType;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Installer Manage Model
*
* @since 1.5
*/
class ManageModel extends InstallerModel
{
/**
* Constructor.
*
* @param array $config An optional associative array of configuration settings.
* @param MVCFactoryInterface $factory The factory.
*
* @see \Joomla\CMS\MVC\Model\ListModel
* @since 1.6
*/
public function __construct($config = [], MVCFactoryInterface $factory = null)
{
if (empty($config['filter_fields'])) {
$config['filter_fields'] = [
'status',
'name',
'client_id',
'client', 'client_translated',
'type', 'type_translated',
'folder', 'folder_translated',
'package_id',
'extension_id',
'creationDate',
'core',
];
}
parent::__construct($config, $factory);
}
/**
* 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
*
* @throws \Exception
*
* @since 1.6
*/
protected function populateState($ordering = 'name', $direction = 'asc')
{
$app = Factory::getApplication();
// Load the filter state.
$this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string'));
$this->setState('filter.client_id', $this->getUserStateFromRequest($this->context . '.filter.client_id', 'filter_client_id', null, 'int'));
$this->setState('filter.package_id', $this->getUserStateFromRequest($this->context . '.filter.package_id', 'filter_package_id', null, 'int'));
$this->setState('filter.status', $this->getUserStateFromRequest($this->context . '.filter.status', 'filter_status', '', 'string'));
$this->setState('filter.type', $this->getUserStateFromRequest($this->context . '.filter.type', 'filter_type', '', 'string'));
$this->setState('filter.folder', $this->getUserStateFromRequest($this->context . '.filter.folder', 'filter_folder', '', 'string'));
$this->setState('filter.core', $this->getUserStateFromRequest($this->context . '.filter.core', 'filter_core', '', 'string'));
$this->setState('message', $app->getUserState('com_installer.message'));
$this->setState('extension_message', $app->getUserState('com_installer.extension_message'));
$app->setUserState('com_installer.message', '');
$app->setUserState('com_installer.extension_message', '');
parent::populateState($ordering, $direction);
}
/**
* Enable/Disable an extension.
*
* @param array $eid Extension ids to un/publish
* @param int $value Publish value
*
* @return boolean True on success
*
* @throws \Exception
*
* @since 1.5
*/
public function publish(&$eid = [], $value = 1)
{
if (!$this->getCurrentUser()->authorise('core.edit.state', 'com_installer')) {
Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'error');
return false;
}
$result = true;
/*
* Ensure eid is an array of extension ids
* @todo: If it isn't an array do we want to set an error and fail?
*/
if (!\is_array($eid)) {
$eid = [$eid];
}
// Get a table object for the extension type
$table = new Extension($this->getDatabase());
$context = $this->option . '.' . $this->name;
$dispatcher = $this->getDispatcher();
PluginHelper::importPlugin('extension', null, true, $dispatcher);
// Enable the extension in the table and store it in the database
foreach ($eid as $i => $id) {
$table->load($id);
if ($table->type == 'template') {
$style = new StyleTable($this->getDatabase());
if ($style->load(['template' => $table->element, 'client_id' => $table->client_id, 'home' => 1])) {
Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_ERROR_DISABLE_DEFAULT_TEMPLATE_NOT_PERMITTED'), 'notice');
unset($eid[$i]);
continue;
}
// Parent template cannot be disabled if there are children
if ($style->load(['parent' => $table->element, 'client_id' => $table->client_id])) {
Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_ERROR_DISABLE_PARENT_TEMPLATE_NOT_PERMITTED'), 'notice');
unset($eid[$i]);
continue;
}
}
if ($table->protected == 1) {
$result = false;
Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'error');
} else {
$table->enabled = $value;
}
// Trigger the before change state event.
$dispatcher->dispatch('onExtensionChangeState', new BeforeChangeStateEvent('onExtensionChangeState', [
'context' => $context,
'subject' => $eid,
'value' => $value,
]));
if (!$table->store()) {
$this->setError($table->getError());
$result = false;
}
}
// Clear the cached extension data and menu cache
$this->cleanCache('_system');
$this->cleanCache('com_modules');
$this->cleanCache('mod_menu');
return $result;
}
/**
* Refreshes the cached manifest information for an extension.
*
* @param int|int[] $eid extension identifier (key in #__extensions)
*
* @return boolean result of refresh
*
* @since 1.6
*/
public function refresh($eid)
{
if (!\is_array($eid)) {
$eid = [$eid => 0];
}
// Get an installer object for the extension type
$installer = Installer::getInstance();
$result = 0;
// Uninstall the chosen extensions
foreach ($eid as $id) {
$result |= $installer->refreshManifestCache($id);
}
return $result;
}
/**
* Remove (uninstall) an extension
*
* @param array $eid An array of identifiers
*
* @return boolean True on success
*
* @throws \Exception
*
* @since 1.5
*/
public function remove($eid = [])
{
if (!$this->getCurrentUser()->authorise('core.delete', 'com_installer')) {
Factory::getApplication()->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'error');
return false;
}
/*
* Ensure eid is an array of extension ids in the form id => client_id
* @todo: If it isn't an array do we want to set an error and fail?
*/
if (!\is_array($eid)) {
$eid = [$eid => 0];
}
// Get an installer object for the extension type
$installer = Installer::getInstance();
$row = new \Joomla\CMS\Table\Extension($this->getDatabase());
// Uninstall the chosen extensions
$msgs = [];
$result = false;
foreach ($eid as $id) {
$id = trim($id);
$row->load($id);
$result = false;
// Do not allow to uninstall locked extensions.
if ((int) $row->locked === 1) {
$msgs[] = Text::sprintf('COM_INSTALLER_UNINSTALL_ERROR_LOCKED_EXTENSION', $row->name, $id);
continue;
}
$langstring = 'COM_INSTALLER_TYPE_TYPE_' . strtoupper($row->type);
$rowtype = Text::_($langstring);
if (strpos($rowtype, $langstring) !== false) {
$rowtype = $row->type;
}
if ($row->type) {
$result = $installer->uninstall($row->type, $id);
// Build an array of extensions that failed to uninstall
if ($result === false) {
// There was an error in uninstalling the package
$msgs[] = Text::sprintf('COM_INSTALLER_UNINSTALL_ERROR', $rowtype);
continue;
}
// Package uninstalled successfully
$msgs[] = Text::sprintf('COM_INSTALLER_UNINSTALL_SUCCESS', $rowtype);
$result = true;
continue;
}
// There was an error in uninstalling the package
$msgs[] = Text::sprintf('COM_INSTALLER_UNINSTALL_ERROR', $rowtype);
}
$msg = implode('<br>', $msgs);
$app = Factory::getApplication();
$app->enqueueMessage($msg);
$this->setState('action', 'remove');
$this->setState('name', $installer->get('name'));
$app->setUserState('com_installer.message', $installer->message);
$app->setUserState('com_installer.extension_message', $installer->get('extension_message'));
// Clear the cached extension data and menu cache
$this->cleanCache('_system');
$this->cleanCache('com_modules');
$this->cleanCache('com_plugins');
$this->cleanCache('mod_menu');
return $result;
}
/**
* Method to get the database query
*
* @return DatabaseQuery The database query
*
* @since 1.6
*/
protected function getListQuery()
{
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select('*')
->select('2*protected+(1-protected)*enabled AS status')
->from('#__extensions')
->where('state = 0');
// Process select filters.
$status = $this->getState('filter.status', '');
$type = $this->getState('filter.type');
$clientId = $this->getState('filter.client_id', '');
$folder = $this->getState('filter.folder');
$core = $this->getState('filter.core', '');
$packageId = $this->getState('filter.package_id', '');
if ($status !== '') {
if ($status === '2') {
$query->where('protected = 1');
} elseif ($status === '3') {
$query->where('protected = 0');
} else {
$status = (int) $status;
$query->where($db->quoteName('protected') . ' = 0')
->where($db->quoteName('enabled') . ' = :status')
->bind(':status', $status, ParameterType::INTEGER);
}
}
if ($type) {
$query->where($db->quoteName('type') . ' = :type')
->bind(':type', $type);
}
if ($clientId !== '') {
$clientId = (int) $clientId;
$query->where($db->quoteName('client_id') . ' = :clientid')
->bind(':clientid', $clientId, ParameterType::INTEGER);
}
if ($packageId !== '') {
$packageId = (int) $packageId;
$query->where(
'((' . $db->quoteName('package_id') . ' = :packageId1) OR '
. '(' . $db->quoteName('extension_id') . ' = :packageId2))'
)
->bind([':packageId1',':packageId2'], $packageId, ParameterType::INTEGER);
}
if ($folder) {
$folder = $folder === '*' ? '' : $folder;
$query->where($db->quoteName('folder') . ' = :folder')
->bind(':folder', $folder);
}
// Filter by core extensions.
if ($core === '1' || $core === '0') {
$coreExtensionIds = ExtensionHelper::getCoreExtensionIds();
$method = $core === '1' ? 'whereIn' : 'whereNotIn';
$query->$method($db->quoteName('extension_id'), $coreExtensionIds);
}
// Process search filter (extension id).
$search = $this->getState('filter.search');
if (!empty($search) && stripos($search, 'id:') === 0) {
$ids = (int) substr($search, 3);
$query->where($db->quoteName('extension_id') . ' = :eid')
->bind(':eid', $ids, ParameterType::INTEGER);
}
// Note: The search for name, ordering and pagination are processed by the parent InstallerModel class (in extension.php).
return $query;
}
/**
* Load the changelog details for a given extension.
*
* @param integer $eid The extension ID
* @param string $source The view the changelog is for, this is used to determine which version number to show
*
* @return string The output to show in the modal.
*
* @since 4.0.0
*/
public function loadChangelog($eid, $source)
{
// Get the changelog URL
$eid = (int) $eid;
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select(
$db->quoteName(
[
'extensions.element',
'extensions.type',
'extensions.folder',
'extensions.changelogurl',
'extensions.manifest_cache',
'extensions.client_id',
]
)
)
->select($db->quoteName('updates.version', 'updateVersion'))
->from($db->quoteName('#__extensions', 'extensions'))
->join(
'LEFT',
$db->quoteName('#__updates', 'updates'),
$db->quoteName('updates.extension_id') . ' = ' . $db->quoteName('extensions.extension_id')
)
->where($db->quoteName('extensions.extension_id') . ' = :eid')
->bind(':eid', $eid, ParameterType::INTEGER);
$db->setQuery($query);
$extensions = $db->loadObjectList();
$this->translate($extensions);
$extension = array_shift($extensions);
if (!$extension->changelogurl) {
return '';
}
$changelog = new Changelog();
$changelog->setVersion($source === 'manage' ? $extension->version : $extension->updateVersion);
$changelog->loadFromXml($extension->changelogurl);
// Read all the entries
$entries = [
'security' => [],
'fix' => [],
'addition' => [],
'change' => [],
'remove' => [],
'language' => [],
'note' => [],
];
foreach (array_keys($entries) as $name) {
$field = $changelog->get($name);
if ($field) {
$entries[$name] = $changelog->get($name)->data;
}
}
$layout = new FileLayout('joomla.installer.changelog');
$output = $layout->render($entries);
return $output;
}
}

View File

@ -0,0 +1,613 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_installer
*
* @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\Component\Installer\Administrator\Model;
use Joomla\CMS\Extension\ExtensionHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\Form\Form;
use Joomla\CMS\Installer\Installer;
use Joomla\CMS\Installer\InstallerHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\CMS\MVC\Model\ListModel;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\Updater\Update;
use Joomla\CMS\Updater\Updater;
use Joomla\Database\DatabaseQuery;
use Joomla\Database\Exception\ExecutionFailureException;
use Joomla\Database\ParameterType;
use Joomla\Utilities\ArrayHelper;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Installer Update Model
*
* @since 1.6
*/
class UpdateModel extends ListModel
{
/**
* Constructor.
*
* @param array $config An optional associative array of configuration settings.
* @param MVCFactoryInterface $factory The factory.
*
* @see \Joomla\CMS\MVC\Model\ListModel
* @since 1.6
*/
public function __construct($config = [], MVCFactoryInterface $factory = null)
{
if (empty($config['filter_fields'])) {
$config['filter_fields'] = [
'name', 'u.name',
'client_id', 'u.client_id', 'client_translated',
'type', 'u.type', 'type_translated',
'folder', 'u.folder', 'folder_translated',
'extension_id', 'u.extension_id',
];
}
parent::__construct($config, $factory);
}
/**
* 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 1.6
*/
protected function populateState($ordering = 'u.name', $direction = 'asc')
{
$this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string'));
$this->setState('filter.client_id', $this->getUserStateFromRequest($this->context . '.filter.client_id', 'filter_client_id', null, 'int'));
$this->setState('filter.type', $this->getUserStateFromRequest($this->context . '.filter.type', 'filter_type', '', 'string'));
$this->setState('filter.folder', $this->getUserStateFromRequest($this->context . '.filter.folder', 'filter_folder', '', 'string'));
$app = Factory::getApplication();
$this->setState('message', $app->getUserState('com_installer.message'));
$this->setState('extension_message', $app->getUserState('com_installer.extension_message'));
$app->setUserState('com_installer.message', '');
$app->setUserState('com_installer.extension_message', '');
parent::populateState($ordering, $direction);
}
/**
* Method to get the database query
*
* @return \Joomla\Database\DatabaseQuery The database query
*
* @since 1.6
*/
protected function getListQuery()
{
$db = $this->getDatabase();
// Grab updates ignoring new installs
$query = $db->getQuery(true)
->select('u.*')
->select($db->quoteName('e.manifest_cache'))
->from($db->quoteName('#__updates', 'u'))
->join(
'LEFT',
$db->quoteName('#__extensions', 'e'),
$db->quoteName('e.extension_id') . ' = ' . $db->quoteName('u.extension_id')
)
->where($db->quoteName('u.extension_id') . ' != 0');
// Process select filters.
$clientId = $this->getState('filter.client_id');
$type = $this->getState('filter.type');
$folder = $this->getState('filter.folder');
$extensionId = $this->getState('filter.extension_id');
if ($type) {
$query->where($db->quoteName('u.type') . ' = :type')
->bind(':type', $type);
}
if ($clientId != '') {
$clientId = (int) $clientId;
$query->where($db->quoteName('u.client_id') . ' = :clientid')
->bind(':clientid', $clientId, ParameterType::INTEGER);
}
if ($folder != '' && \in_array($type, ['plugin', 'library', ''])) {
$folder = $folder === '*' ? '' : $folder;
$query->where($db->quoteName('u.folder') . ' = :folder')
->bind(':folder', $folder);
}
if ($extensionId) {
$extensionId = (int) $extensionId;
$query->where($db->quoteName('u.extension_id') . ' = :extensionid')
->bind(':extensionid', $extensionId, ParameterType::INTEGER);
} else {
$eid = ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id;
$query->where($db->quoteName('u.extension_id') . ' != 0')
->where($db->quoteName('u.extension_id') . ' != :eid')
->bind(':eid', $eid, ParameterType::INTEGER);
}
// Process search filter.
$search = $this->getState('filter.search');
if (!empty($search)) {
if (stripos($search, 'eid:') !== false) {
$sid = (int) substr($search, 4);
$query->where($db->quoteName('u.extension_id') . ' = :sid')
->bind(':sid', $sid, ParameterType::INTEGER);
} else {
if (stripos($search, 'uid:') !== false) {
$suid = (int) substr($search, 4);
$query->where($db->quoteName('u.update_site_id') . ' = :suid')
->bind(':suid', $suid, ParameterType::INTEGER);
} elseif (stripos($search, 'id:') !== false) {
$uid = (int) substr($search, 3);
$query->where($db->quoteName('u.update_id') . ' = :uid')
->bind(':uid', $uid, ParameterType::INTEGER);
} else {
$search = '%' . str_replace(' ', '%', trim($search)) . '%';
$query->where($db->quoteName('u.name') . ' LIKE :search')
->bind(':search', $search);
}
}
}
return $query;
}
/**
* Translate a list of objects
*
* @param array $items The array of objects
*
* @return array The array of translated objects
*
* @since 3.5
*/
protected function translate(&$items)
{
foreach ($items as &$item) {
$item->client_translated = Text::_([0 => 'JSITE', 1 => 'JADMINISTRATOR', 3 => 'JAPI'][$item->client_id] ?? 'JSITE');
$manifest = json_decode($item->manifest_cache);
$item->current_version = $manifest->version ?? Text::_('JLIB_UNKNOWN');
$item->description = $item->description !== '' ? $item->description : Text::_('COM_INSTALLER_MSG_UPDATE_NODESC');
$item->type_translated = Text::_('COM_INSTALLER_TYPE_' . strtoupper($item->type));
$item->folder_translated = $item->folder ?: Text::_('COM_INSTALLER_TYPE_NONAPPLICABLE');
$item->install_type = $item->extension_id ? Text::_('COM_INSTALLER_MSG_UPDATE_UPDATE') : Text::_('COM_INSTALLER_NEW_INSTALL');
}
return $items;
}
/**
* Returns an object list
*
* @param DatabaseQuery $query The query
* @param int $limitstart Offset
* @param int $limit The number of records
*
* @return object[]
*
* @since 3.5
*/
protected function _getList($query, $limitstart = 0, $limit = 0)
{
$db = $this->getDatabase();
$listOrder = $this->getState('list.ordering', 'u.name');
$listDirn = $this->getState('list.direction', 'asc');
// Process ordering.
if (\in_array($listOrder, ['client_translated', 'folder_translated', 'type_translated'])) {
$db->setQuery($query);
$result = $db->loadObjectList();
$this->translate($result);
$result = ArrayHelper::sortObjects($result, $listOrder, strtolower($listDirn) === 'desc' ? -1 : 1, true, true);
$total = \count($result);
if ($total < $limitstart) {
$limitstart = 0;
$this->setState('list.start', 0);
}
return \array_slice($result, $limitstart, $limit ?: null);
}
$query->order($db->quoteName($listOrder) . ' ' . $db->escape($listDirn));
$result = parent::_getList($query, $limitstart, $limit);
$this->translate($result);
return $result;
}
/**
* Get the count of disabled update sites
*
* @return integer
*
* @since 3.4
*/
public function getDisabledUpdateSites()
{
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__update_sites'))
->where($db->quoteName('enabled') . ' = 0');
$db->setQuery($query);
return $db->loadResult();
}
/**
* Finds updates for an extension.
*
* @param int $eid Extension identifier to look for
* @param int $cacheTimeout Cache timeout
* @param int $minimumStability Minimum stability for updates {@see Updater} (0=dev, 1=alpha, 2=beta, 3=rc, 4=stable)
*
* @return boolean Result
*
* @since 1.6
*/
public function findUpdates($eid = 0, $cacheTimeout = 0, $minimumStability = Updater::STABILITY_STABLE)
{
Updater::getInstance()->findUpdates($eid, $cacheTimeout, $minimumStability);
return true;
}
/**
* Removes all of the updates from the table.
*
* @return boolean result of operation
*
* @since 1.6
*/
public function purge()
{
$db = $this->getDatabase();
try {
$db->truncateTable('#__updates');
} catch (ExecutionFailureException $e) {
$this->_message = Text::_('JLIB_INSTALLER_FAILED_TO_PURGE_UPDATES');
return false;
}
// Reset the last update check timestamp
$query = $db->getQuery(true)
->update($db->quoteName('#__update_sites'))
->set($db->quoteName('last_check_timestamp') . ' = ' . $db->quote(0));
$db->setQuery($query);
$db->execute();
// Clear the administrator cache
$this->cleanCache('_system');
$this->_message = Text::_('JLIB_INSTALLER_PURGED_UPDATES');
return true;
}
/**
* Update function.
*
* Sets the "result" state with the result of the operation.
*
* @param int[] $uids List of updates to apply
* @param int $minimumStability The minimum allowed stability for installed updates {@see Updater}
*
* @return void
*
* @since 1.6
*/
public function update($uids, $minimumStability = Updater::STABILITY_STABLE)
{
$result = true;
foreach ($uids as $uid) {
$update = new Update();
$instance = new \Joomla\CMS\Table\Update($this->getDatabase());
if (!$instance->load($uid)) {
// Update no longer available, maybe already updated by a package.
continue;
}
$update->loadFromXml($instance->detailsurl, $minimumStability);
// Find and use extra_query from update_site if available
$updateSiteInstance = new \Joomla\CMS\Table\UpdateSite($this->getDatabase());
$updateSiteInstance->load($instance->update_site_id);
if ($updateSiteInstance->extra_query) {
$update->set('extra_query', $updateSiteInstance->extra_query);
}
$this->preparePreUpdate($update, $instance);
// Install sets state and enqueues messages
$res = $this->install($update);
if ($res) {
$instance->delete($uid);
}
$result = $res & $result;
}
// Clear the cached extension data and menu cache
$this->cleanCache('_system');
$this->cleanCache('com_modules');
$this->cleanCache('com_plugins');
$this->cleanCache('mod_menu');
// Set the final state
$this->setState('result', $result);
}
/**
* Handles the actual update installation.
*
* @param Update $update An update definition
*
* @return boolean Result of install
*
* @since 1.6
*/
private function install($update)
{
// Load overrides plugin.
PluginHelper::importPlugin('installer');
$app = Factory::getApplication();
if (!isset($update->get('downloadurl')->_data)) {
Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_INVALID_EXTENSION_UPDATE'), 'error');
return false;
}
$url = trim($update->downloadurl->_data);
$sources = $update->get('downloadSources', []);
if ($extra_query = $update->get('extra_query')) {
$url .= (strpos($url, '?') === false) ? '?' : '&amp;';
$url .= $extra_query;
}
$mirror = 0;
while (!($p_file = InstallerHelper::downloadPackage($url)) && isset($sources[$mirror])) {
$name = $sources[$mirror];
$url = trim($name->url);
if ($extra_query) {
$url .= (strpos($url, '?') === false) ? '?' : '&amp;';
$url .= $extra_query;
}
$mirror++;
}
// Was the package downloaded?
if (!$p_file) {
Factory::getApplication()->enqueueMessage(Text::sprintf('COM_INSTALLER_PACKAGE_DOWNLOAD_FAILED', $url), 'error');
return false;
}
$config = $app->getConfig();
$tmp_dest = $config->get('tmp_path');
// Unpack the downloaded package file
$package = InstallerHelper::unpack($tmp_dest . '/' . $p_file);
if (empty($package)) {
$app->enqueueMessage(Text::sprintf('COM_INSTALLER_UNPACK_ERROR', $p_file), 'error');
return false;
}
// Get an installer instance
$installer = Installer::getInstance();
$update->set('type', $package['type']);
// Check the package
$check = InstallerHelper::isChecksumValid($package['packagefile'], $update);
if ($check === InstallerHelper::HASH_NOT_VALIDATED) {
$app->enqueueMessage(Text::_('COM_INSTALLER_INSTALL_CHECKSUM_WRONG'), 'error');
return false;
}
if ($check === InstallerHelper::HASH_NOT_PROVIDED) {
$app->enqueueMessage(Text::_('COM_INSTALLER_INSTALL_CHECKSUM_WARNING'), 'warning');
}
// Install the package
if (!$installer->update($package['dir'])) {
// There was an error updating the package
$app->enqueueMessage(
Text::sprintf(
'COM_INSTALLER_MSG_UPDATE_ERROR',
Text::_('COM_INSTALLER_TYPE_TYPE_' . strtoupper($package['type']))
),
'error'
);
$result = false;
} else {
// Package updated successfully
$app->enqueueMessage(
Text::sprintf(
'COM_INSTALLER_MSG_UPDATE_SUCCESS',
Text::_('COM_INSTALLER_TYPE_TYPE_' . strtoupper($package['type']))
),
'success'
);
$result = true;
}
// Quick change
$this->type = $package['type'];
$this->setState('name', $installer->get('name'));
$this->setState('result', $result);
$app->setUserState('com_installer.message', $installer->message);
$app->setUserState('com_installer.extension_message', $installer->get('extension_message'));
// Cleanup the install files
if (!is_file($package['packagefile'])) {
$package['packagefile'] = $config->get('tmp_path') . '/' . $package['packagefile'];
}
InstallerHelper::cleanupInstall($package['packagefile'], $package['extractdir']);
return $result;
}
/**
* Method to get the row form.
*
* @param array $data Data for the form.
* @param boolean $loadData True if the form is to load its own data (default case), false if not.
*
* @return Form|bool A Form object on success, false on failure
*
* @since 2.5.2
*/
public function getForm($data = [], $loadData = true)
{
// Get the form.
Form::addFormPath(JPATH_COMPONENT . '/models/forms');
Form::addFieldPath(JPATH_COMPONENT . '/models/fields');
$form = Form::getInstance('com_installer.update', 'update', ['load_data' => $loadData]);
// Check for an error.
if ($form == false) {
$this->setError($form->getMessage());
return false;
}
// Check the session for previously entered form data.
$data = $this->loadFormData();
// Bind the form data if present.
if (!empty($data)) {
$form->bind($data);
}
return $form;
}
/**
* Method to get the data that should be injected in the form.
*
* @return mixed The data for the form.
*
* @since 2.5.2
*/
protected function loadFormData()
{
// Check the session for previously entered form data.
$data = Factory::getApplication()->getUserState($this->context, []);
return $data;
}
/**
* Method to add parameters to the update
*
* @param Update $update An update definition
* @param \Joomla\CMS\Table\Update $table The update instance from the database
*
* @return void
*
* @since 3.7.0
*/
protected function preparePreUpdate($update, $table)
{
switch ($table->type) {
// Components could have a helper which adds additional data
case 'component':
$ename = str_replace('com_', '', $table->element);
$fname = $ename . '.php';
$cname = ucfirst($ename) . 'Helper';
$path = JPATH_ADMINISTRATOR . '/components/' . $table->element . '/helpers/' . $fname;
if (is_file($path)) {
require_once $path;
if (class_exists($cname) && \is_callable([$cname, 'prepareUpdate'])) {
\call_user_func_array([$cname, 'prepareUpdate'], [&$update, &$table]);
}
}
break;
// Modules could have a helper which adds additional data
case 'module':
$cname = str_replace('_', '', $table->element) . 'Helper';
$path = ($table->client_id ? JPATH_ADMINISTRATOR : JPATH_SITE) . '/modules/' . $table->element . '/helper.php';
if (is_file($path)) {
require_once $path;
if (class_exists($cname) && \is_callable([$cname, 'prepareUpdate'])) {
\call_user_func_array([$cname, 'prepareUpdate'], [&$update, &$table]);
}
}
break;
// If we have a plugin, we can use the plugin trigger "onInstallerBeforePackageDownload"
// But we should make sure, that our plugin is loaded, so we don't need a second "installer" plugin
case 'plugin':
$cname = str_replace('plg_', '', $table->element);
PluginHelper::importPlugin($table->folder, $cname);
break;
}
}
/**
* Manipulate the query to be used to evaluate if this is an Empty State to provide specific conditions for this extension.
*
* @return DatabaseQuery
*
* @since 4.0.0
*/
protected function getEmptyStateQuery()
{
$query = parent::getEmptyStateQuery();
$query->where($this->getDatabase()->quoteName('extension_id') . ' != 0');
return $query;
}
}

View File

@ -0,0 +1,170 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_installer
*
* @copyright (C) 2019 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\Form\Form;
use Joomla\CMS\MVC\Model\AdminModel;
use Joomla\CMS\Object\CMSObject;
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
/**
* Item Model for an update site.
*
* @since 4.0.0
*/
class UpdatesiteModel extends AdminModel
{
/**
* The type alias for this content type.
*
* @var string
* @since 4.0.0
*/
public $typeAlias = 'com_installer.updatesite';
/**
* Method to get the row form.
*
* @param array $data Data for the form.
* @param boolean $loadData True if the form is to load its own data (default case), false if not.
*
* @return Form|boolean A Form object on success, false on failure
*
* @throws \Exception
*
* @since 4.0.0
*/
public function getForm($data = [], $loadData = true)
{
// Get the form.
$form = $this->loadForm('com_installer.updatesite', 'updatesite', ['control' => 'jform', 'load_data' => $loadData]);
if (empty($form)) {
return false;
}
return $form;
}
/**
* Method to get the data that should be injected in the form.
*
* @return mixed The data for the form.
*
* @since 4.0.0
*/
protected function loadFormData()
{
$data = $this->getItem();
$this->preprocessData('com_installer.updatesite', $data);
return $data;
}
/**
* Method to get a single record.
*
* @param integer $pk The id of the primary key.
*
* @return CMSObject|boolean Object on success, false on failure.
*
* @since 4.0.0
*/
public function getItem($pk = null)
{
$item = parent::getItem($pk);
$db = $this->getDatabase();
$updateSiteId = (int) $item->get('update_site_id');
$query = $db->getQuery(true)
->select(
$db->quoteName(
[
'update_sites.extra_query',
'extensions.type',
'extensions.element',
'extensions.folder',
'extensions.client_id',
'extensions.checked_out',
]
)
)
->from($db->quoteName('#__update_sites', 'update_sites'))
->join(
'INNER',
$db->quoteName('#__update_sites_extensions', 'update_sites_extensions'),
$db->quoteName('update_sites_extensions.update_site_id') . ' = ' . $db->quoteName('update_sites.update_site_id')
)
->join(
'INNER',
$db->quoteName('#__extensions', 'extensions'),
$db->quoteName('extensions.extension_id') . ' = ' . $db->quoteName('update_sites_extensions.extension_id')
)
->where($db->quoteName('update_sites.update_site_id') . ' = :updatesiteid')
->bind(':updatesiteid', $updateSiteId, ParameterType::INTEGER);
$db->setQuery($query);
$extension = new CMSObject($db->loadAssoc());
$downloadKey = InstallerHelper::getDownloadKey($extension);
$item->set('extra_query', $downloadKey['value'] ?? '');
$item->set('downloadIdPrefix', $downloadKey['prefix'] ?? '');
$item->set('downloadIdSuffix', $downloadKey['suffix'] ?? '');
return $item;
}
/**
* Method to save the form data.
*
* @param array $data The form data.
*
* @return boolean True on success, False on error.
*
* @since 4.0.0
*/
public function save($data): bool
{
// Apply the extra_query. Always empty when saving a free extension's update site.
if (isset($data['extra_query'])) {
$data['extra_query'] = $data['downloadIdPrefix'] . $data['extra_query'] . $data['downloadIdSuffix'];
}
// Force Joomla to recheck for updates
$data['last_check_timestamp'] = 0;
$result = parent::save($data);
if (!$result) {
return $result;
}
// Delete update records forcing Joomla to fetch them again, applying the new extra_query.
$db = $this->getDatabase();
$query = $db->getQuery(true)
->delete($db->quoteName('#__updates'))
->where($db->quoteName('update_site_id') . ' = :updateSiteId');
$query->bind(':updateSiteId', $data['update_site_id'], ParameterType::INTEGER);
try {
$db->setQuery($query)->execute();
} catch (\Exception $e) {
// No problem if this fails for any reason.
}
return true;
}
}

View File

@ -0,0 +1,661 @@
<?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;
}
}

View File

@ -0,0 +1,182 @@
<?php
/**
* @package Joomla.Administrator
* @subpackage com_installer
*
* @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\Component\Installer\Administrator\Model;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Model\ListModel;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Installer Warnings Model
*
* @since 1.6
*/
class WarningsModel extends ListModel
{
/**
* Extension Type
* @var string
*/
public $type = 'warnings';
/**
* Return the byte value of a particular string.
*
* @param string $val String optionally with G, M or K suffix
*
* @return integer size in bytes
*
* @since 1.6
*/
public function return_bytes($val)
{
if (empty($val)) {
return 0;
}
$val = trim($val);
preg_match('#([0-9]+)[\s]*([a-z]+)#i', $val, $matches);
$last = '';
if (isset($matches[2])) {
$last = $matches[2];
}
if (isset($matches[1])) {
$val = (int) $matches[1];
}
switch (strtolower($last)) {
case 'g':
case 'gb':
$val *= (1024 * 1024 * 1024);
break;
case 'm':
case 'mb':
$val *= (1024 * 1024);
break;
case 'k':
case 'kb':
$val *= 1024;
break;
}
return (int) $val;
}
/**
* Load the data.
*
* @return array Messages
*
* @since 1.6
*/
public function getItems()
{
static $messages;
if ($messages) {
return $messages;
}
$messages = [];
// 16MB
$minLimit = 16 * 1024 * 1024;
$file_uploads = ini_get('file_uploads');
if (!$file_uploads) {
$messages[] = [
'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_FILEUPLOADSDISABLED'),
'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_FILEUPLOADISDISABLEDDESC'),
];
}
$upload_dir = ini_get('upload_tmp_dir');
if (!$upload_dir) {
$messages[] = [
'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_PHPUPLOADNOTSET'),
'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_PHPUPLOADNOTSETDESC'),
];
} elseif (!is_writable($upload_dir)) {
$messages[] = [
'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_PHPUPLOADNOTWRITEABLE'),
'description' => Text::sprintf('COM_INSTALLER_MSG_WARNINGS_PHPUPLOADNOTWRITEABLEDESC', $upload_dir),
];
}
$tmp_path = Factory::getApplication()->get('tmp_path');
if (!$tmp_path) {
$messages[] = [
'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTSET'),
'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTSETDESC'),
];
} elseif (!is_writable($tmp_path)) {
$messages[] = [
'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTWRITEABLE'),
'description' => Text::sprintf('COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTWRITEABLEDESC', $tmp_path),
];
}
$memory_limit = $this->return_bytes(ini_get('memory_limit'));
if ($memory_limit > -1) {
if ($memory_limit < $minLimit) {
// 16MB
$messages[] = [
'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_LOWMEMORYWARN'),
'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_LOWMEMORYDESC'),
];
} elseif ($memory_limit < ($minLimit * 1.5)) {
// 24MB
$messages[] = [
'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_MEDMEMORYWARN'),
'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_MEDMEMORYDESC'),
];
}
}
$post_max_size = $this->return_bytes(ini_get('post_max_size'));
$upload_max_filesize = $this->return_bytes(ini_get('upload_max_filesize'));
if ($post_max_size > 0 && $post_max_size < $upload_max_filesize) {
$messages[] = [
'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_UPLOADBIGGERTHANPOST'),
'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_UPLOADBIGGERTHANPOSTDESC'),
];
}
if ($post_max_size > 0 && $post_max_size < $minLimit) {
$messages[] = [
'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_SMALLPOSTSIZE'),
'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_SMALLPOSTSIZEDESC'),
];
}
if ($upload_max_filesize > 0 && $upload_max_filesize < $minLimit) {
$messages[] = [
'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_SMALLUPLOADSIZE'),
'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_SMALLUPLOADSIZEDESC'),
];
}
return $messages;
}
}