diff --git a/administrator/components/com_config/src/Model/ApplicationModel.php b/administrator/components/com_config/src/Model/ApplicationModel.php
index 3aaa6ce171569..2029a903c3876 100644
--- a/administrator/components/com_config/src/Model/ApplicationModel.php
+++ b/administrator/components/com_config/src/Model/ApplicationModel.php
@@ -107,7 +107,7 @@ public function getData()
// Merge in the session data.
if (!empty($temp))
{
- $data = array_merge($data, $temp);
+ $data = array_merge($temp, $data);
}
// Correct error_reporting value, since we removed "development", the "maximum" should be set instead
diff --git a/installation/src/Model/ConfigurationModel.php b/installation/src/Model/ConfigurationModel.php
index bc95874789d15..4bc25f5a5d92d 100644
--- a/installation/src/Model/ConfigurationModel.php
+++ b/installation/src/Model/ConfigurationModel.php
@@ -425,6 +425,7 @@ public function createConfiguration($options)
$registry->set('dbsslcipher', $options->db_sslcipher);
// Server settings.
+ $registry->set('force_ssl', 0);
$registry->set('live_site', '');
$registry->set('secret', UserHelper::genRandomPassword(16));
$registry->set('gzip', false);
diff --git a/libraries/src/Application/ConsoleApplication.php b/libraries/src/Application/ConsoleApplication.php
index a4ea32c06abf6..7008fcf6fa441 100644
--- a/libraries/src/Application/ConsoleApplication.php
+++ b/libraries/src/Application/ConsoleApplication.php
@@ -15,6 +15,7 @@
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Language;
use Joomla\CMS\Plugin\PluginHelper;
+use Joomla\CMS\Version;
use Joomla\Console\Application;
use Joomla\DI\Container;
use Joomla\DI\ContainerAwareTrait;
@@ -387,4 +388,16 @@ public function setSession(SessionInterface $session): self
return $this;
}
+
+ /**
+ * Flush the media version to refresh versionable assets
+ *
+ * @return void
+ *
+ * @since 4.0.0
+ */
+ public function flushAssets()
+ {
+ (new Version)->refreshMediaVersion();
+ }
}
diff --git a/libraries/src/Console/CheckJoomlaUpdatesCommand.php b/libraries/src/Console/CheckJoomlaUpdatesCommand.php
new file mode 100644
index 0000000000000..8d457da430b34
--- /dev/null
+++ b/libraries/src/Console/CheckJoomlaUpdatesCommand.php
@@ -0,0 +1,148 @@
+%command.name% Checks for Joomla updates.
+
+ php %command.full_name%
+EOF;
+ $this->setDescription('Checks for Joomla updates');
+ $this->setHelp($help);
+ }
+
+ /**
+ * Retrieves Update Information
+ *
+ * @return mixed
+ *
+ * @since 4.0
+ */
+ private function getUpdateInformationFromModel()
+ {
+ $app = $this->getApplication();
+ $updatemodel = $app->bootComponent('com_joomlaupdate')->getMVCFactory($app)->createModel('Update', 'Administrator');
+ $updatemodel->purge();
+ $updatemodel->refreshUpdates(true);
+
+ return $updatemodel;
+ }
+
+ /**
+ * Gets the Update Information
+ *
+ * @return mixed
+ *
+ * @since 4.0
+ */
+ public function getUpdateInfo()
+ {
+ if (!$this->updateInfo)
+ {
+ $this->setUpdateInfo();
+ }
+
+ return $this->updateInfo;
+ }
+
+ /**
+ * Sets the Update Information
+ *
+ * @param null $info stores update Information
+ *
+ * @return void
+ *
+ * @since 4.0
+ */
+ public function setUpdateInfo($info = null): void
+ {
+ if (!$info)
+ {
+ $this->updateInfo = $this->getUpdateInformationFromModel();
+ }
+ else
+ {
+ $this->updateInfo = $info;
+ }
+ }
+
+ /**
+ * Internal function to execute the command.
+ *
+ * @param InputInterface $input The input to inject into the command.
+ * @param OutputInterface $output The output to inject into the command.
+ *
+ * @return integer The command exit code
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ protected function doExecute(InputInterface $input, OutputInterface $output): int
+ {
+ $symfonyStyle = new SymfonyStyle($input, $output);
+
+ $model = $this->getUpdateInfo();
+ $data = $model->getUpdateInformation();
+ $symfonyStyle->title('Joomla! Updates');
+
+ if (!$data['hasUpdate'])
+ {
+ $symfonyStyle->success('You already have the latest Joomla version ' . $data['latest']);
+
+ return 0;
+ }
+
+ $symfonyStyle->note('New Joomla Version ' . $data['latest'] . ' is available.');
+
+ if (!isset($data['object']->downloadurl->_data))
+ {
+ $symfonyStyle->warning('We cannot find an update URL');
+ }
+
+ return 0;
+ }
+}
diff --git a/libraries/src/Console/ExtensionInstallCommand.php b/libraries/src/Console/ExtensionInstallCommand.php
new file mode 100644
index 0000000000000..09f3d680305a4
--- /dev/null
+++ b/libraries/src/Console/ExtensionInstallCommand.php
@@ -0,0 +1,229 @@
+cliInput = $input;
+ $this->ioStyle = new SymfonyStyle($input, $output);
+ }
+
+ /**
+ * Initialise the command.
+ *
+ * @return void
+ *
+ * @since 4.0.0
+ */
+ protected function configure(): void
+ {
+ $this->addOption('path', null, InputOption::VALUE_REQUIRED, 'The path to the extension');
+ $this->addOption('url', null, InputOption::VALUE_REQUIRED, 'The url to the extension');
+
+ $this->setDescription('Installs an extension from a URL or from a Path.');
+
+ $help = <<<'EOF'
+The %command.name% is used to install extensions
+
+ php %command.full_name%
+
+You must provide one of the following options to the command:
+
+ --path: The path on your local filesystem to the install package
+ --url: The URL from where the install package should be downloaded
+
+ php %command.full_name% --path=
+ php %command.full_name% --url=
+EOF;
+ $this->setHelp($help);
+ }
+
+ /**
+ * Used for installing extension from a path
+ *
+ * @param string $path Path to the extension zip file
+ *
+ * @return boolean
+ *
+ * @since 4.0
+ *
+ * @throws \Exception
+ */
+ public function processPathInstallation($path): bool
+ {
+ if (!file_exists($path))
+ {
+ $this->ioStyle->warning('The file path specified does not exist.');
+
+ return false;
+ }
+
+ $tmpPath = $this->getApplication()->get('tmp_path');
+ $tmpPath = $tmpPath . '/' . basename($path);
+ $package = InstallerHelper::unpack($path, true);
+
+ if ($package['type'] === false)
+ {
+ return false;
+ }
+
+ $jInstaller = Installer::getInstance();
+ $result = $jInstaller->install($package['extractdir']);
+ InstallerHelper::cleanupInstall($tmpPath, $package['extractdir']);
+
+ return $result;
+ }
+
+
+ /**
+ * Used for installing extension from a URL
+ *
+ * @param string $url URL to the extension zip file
+ *
+ * @return boolean
+ *
+ * @since 4.0
+ *
+ * @throws \Exception
+ */
+ public function processUrlInstallation($url): bool
+ {
+ $filename = InstallerHelper::downloadPackage($url);
+
+ $tmpPath = $this->getApplication()->get('tmp_path');
+
+ $path = $tmpPath . '/' . basename($filename);
+ $package = InstallerHelper::unpack($path, true);
+
+ if ($package['type'] === false)
+ {
+ return false;
+ }
+
+ $jInstaller = new Installer;
+ $result = $jInstaller->install($package['extractdir']);
+ InstallerHelper::cleanupInstall($path, $package['extractdir']);
+
+ return $result;
+ }
+
+ /**
+ * Internal function to execute the command.
+ *
+ * @param InputInterface $input The input to inject into the command.
+ * @param OutputInterface $output The output to inject into the command.
+ *
+ * @return integer The command exit code
+ *
+ * @throws \Exception
+ * @since __DEPLOY_VERSION__
+ */
+ protected function doExecute(InputInterface $input, OutputInterface $output): int
+ {
+ $this->configureIO($input, $output);
+
+ if ($path = $this->cliInput->getOption('path'))
+ {
+ $result = $this->processPathInstallation($path);
+
+ if (!$result)
+ {
+ $this->ioStyle->error('Unable to install extension');
+
+ return self::INSTALLATION_FAILED;
+ }
+
+ $this->ioStyle->success('Extension installed successfully.');
+
+ return self::INSTALLATION_SUCCESSFUL;
+ }
+ elseif ($url = $this->cliInput->getOption('url'))
+ {
+ $result = $this->processUrlInstallation($url);
+
+ if (!$result)
+ {
+ $this->ioStyle->error('Unable to install extension');
+
+ return self::INSTALLATION_FAILED;
+ }
+
+ $this->ioStyle->success('Extension installed successfully.');
+
+ return self::INSTALLATION_SUCCESSFUL;
+ }
+
+ $this->ioStyle->error('Invalid argument supplied for command.');
+
+ return self::INSTALLATION_FAILED;
+ }
+}
diff --git a/libraries/src/Console/ExtensionRemoveCommand.php b/libraries/src/Console/ExtensionRemoveCommand.php
new file mode 100644
index 0000000000000..7a283dab0f727
--- /dev/null
+++ b/libraries/src/Console/ExtensionRemoveCommand.php
@@ -0,0 +1,207 @@
+cliInput = $input;
+ $this->ioStyle = new SymfonyStyle($input, $output);
+ $language = Factory::getLanguage();
+ $language->load('', JPATH_ADMINISTRATOR, null, false, false) ||
+ $language->load('', JPATH_ADMINISTRATOR, null, true);
+ $language->load('com_installer', JPATH_ADMINISTRATOR, null, false, false)||
+ $language->load('com_installer', JPATH_ADMINISTRATOR, null, true);
+ }
+
+ /**
+ * Initialise the command.
+ *
+ * @return void
+ *
+ * @since 4.0.0
+ */
+ protected function configure(): void
+ {
+ $this->addArgument(
+ 'extensionId',
+ InputArgument::REQUIRED,
+ 'ID of extension to be removed (run extension:list command to check)'
+ );
+ $this->setDescription('Removes an extension');
+
+ $help = <<<'EOF'
+The %command.name% is used to uninstall extensions.
+The command requires one argument, the ID of the extension to uninstall.
+You may find this ID by running the extension:list command.
+
+php %command.full_name%
+EOF;
+ $this->setHelp($help);
+ }
+
+ /**
+ * Internal function to execute the command.
+ *
+ * @param InputInterface $input The input to inject into the command.
+ * @param OutputInterface $output The output to inject into the command.
+ *
+ * @return integer The command exit code
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ protected function doExecute(InputInterface $input, OutputInterface $output): int
+ {
+ $this->configureIO($input, $output);
+ $extensionId = $this->cliInput->getArgument('extensionId');
+
+ $response = $this->ioStyle->ask('Are you sure you want to remove this extension?', 'yes/no');
+
+ if (strtolower($response) === 'yes')
+ {
+ // Get an installer object for the extension type
+ $installer = Installer::getInstance();
+ $row = new \Joomla\CMS\Table\Extension(Factory::getDbo());
+
+ if ((int) $extensionId === 0 || !$row->load($extensionId))
+ {
+ $this->ioStyle->error("Extension with ID of $extensionId not found.");
+
+ return self::REMOVE_NOT_FOUND;
+ }
+
+ // Do not allow to uninstall locked extensions.
+ if ((int) $row->locked === 1)
+ {
+ $this->ioStyle->error(Text::_('COM_INSTALLER_UNINSTALL_ERROR_LOCKED_EXTENSION'));
+
+ return self::REMOVE_LOCKED;
+ }
+
+ if ($row->type)
+ {
+ if (!$installer->uninstall($row->type, $extensionId))
+ {
+ $this->ioStyle->error('Extension not removed.');
+
+ return self::REMOVE_FAILED;
+ }
+
+ $this->ioStyle->success('Extension removed!');
+
+ return self::REMOVE_SUCCESSFUL;
+ }
+
+ return self::REMOVE_INVALID_TYPE;
+ }
+ elseif (strtolower($response) === 'no')
+ {
+ $this->ioStyle->note('Extension not removed.');
+
+ return self::REMOVE_ABORT;
+ }
+
+ $this->ioStyle->warning('Invalid response');
+
+ return self::REMOVE_INVALID_RESPONSE;
+ }
+}
diff --git a/libraries/src/Console/ExtensionsListCommand.php b/libraries/src/Console/ExtensionsListCommand.php
new file mode 100644
index 0000000000000..abf5c47691ebc
--- /dev/null
+++ b/libraries/src/Console/ExtensionsListCommand.php
@@ -0,0 +1,264 @@
+db = $db;
+ parent::__construct();
+ }
+
+ /**
+ * Configures the IO
+ *
+ * @param InputInterface $input Console Input
+ * @param OutputInterface $output Console Output
+ *
+ * @return void
+ *
+ * @since 4.0
+ *
+ */
+ private function configureIO(InputInterface $input, OutputInterface $output): void
+ {
+ $this->cliInput = $input;
+ $this->ioStyle = new SymfonyStyle($input, $output);
+ }
+
+ /**
+ * Initialise the command.
+ *
+ * @return void
+ *
+ * @since 4.0.0
+ */
+ protected function configure(): void
+ {
+ $this->setDescription('List installed extensions');
+
+ $this->addOption('type', null, InputOption::VALUE_REQUIRED, 'Type of the extension');
+
+ $help = <<<'EOF'
+The %command.name% is used to list all extensions installed on your site.
+
+ php %command.full_name%
+
+You may filter on the type of extension (component, module, plugin, etc.) using the --type option:
+
+ php %command.full_name% --type=
+EOF;
+ $this->setHelp($help);
+ }
+
+ /**
+ * Retrieves all extensions
+ *
+ * @return mixed
+ *
+ * @since 4.0
+ */
+ public function getExtensions()
+ {
+ if (!$this->extensions)
+ {
+ $this->setExtensions();
+ }
+
+ return $this->extensions;
+ }
+
+ /**
+ * Retrieves the extension from the model and sets the class variable
+ *
+ * @param null $extensions Array of extensions
+ *
+ * @return void
+ *
+ * @since 4.0
+ */
+ public function setExtensions($extensions = null): void
+ {
+ if (!$extensions)
+ {
+ $this->extensions = $this->getAllExtensionsFromDB();
+ }
+ else
+ {
+ $this->extensions = $extensions;
+ }
+ }
+
+ /**
+ * Retrieves extension list from DB
+ *
+ * @return array
+ *
+ * @since 4.0
+ */
+ private function getAllExtensionsFromDB(): array
+ {
+ $db = $this->db;
+ $query = $db->getQuery(true);
+ $query->select('*')
+ ->from('#__extensions');
+ $db->setQuery($query);
+ $extensions = $db->loadAssocList('extension_id');
+
+ return $extensions;
+ }
+
+ /**
+ * Transforms extension arrays into required form
+ *
+ * @param array $extensions Array of extensions
+ *
+ * @return array
+ *
+ * @since 4.0
+ */
+ private function getExtensionsNameAndId($extensions): array
+ {
+ $extInfo = [];
+
+ foreach ($extensions as $key => $extension)
+ {
+ $manifest = json_decode($extension['manifest_cache']);
+ $extInfo[] = [
+ $extension['name'],
+ $extension['extension_id'],
+ $manifest ? $manifest->version : '--',
+ $extension['type'],
+ $extension['enabled'] == 1 ? 'Yes' : 'No',
+ ];
+ }
+
+ return $extInfo;
+ }
+
+ /**
+ * Filters the extension type
+ *
+ * @param string $type Extension type
+ *
+ * @return array
+ *
+ * @since 4.0
+ */
+ private function filterExtensionsBasedOn($type): array
+ {
+ $extensions = [];
+
+ foreach ($this->extensions as $key => $extension)
+ {
+ if ($extension['type'] == $type)
+ {
+ $extensions[] = $extension;
+ }
+ }
+
+ return $extensions;
+ }
+
+ /**
+ * Internal function to execute the command.
+ *
+ * @param InputInterface $input The input to inject into the command.
+ * @param OutputInterface $output The output to inject into the command.
+ *
+ * @return integer The command exit code
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ protected function doExecute(InputInterface $input, OutputInterface $output): int
+ {
+ $this->configureIO($input, $output);
+ $extensions = $this->getExtensions();
+ $type = $this->cliInput->getOption('type');
+
+ if ($type)
+ {
+ $extensions = $this->filterExtensionsBasedOn($type);
+ }
+
+ if (empty($extensions))
+ {
+ $this->ioStyle->error("Cannot find extensions of the type '$type' specified.");
+
+ return 0;
+ }
+
+ $extensions = $this->getExtensionsNameAndId($extensions);
+
+ $this->ioStyle->title('Installed extensions.');
+ $this->ioStyle->table(['Name', 'Extension ID', 'Version', 'Type', 'Active'], $extensions);
+
+ return 0;
+ }
+}
diff --git a/libraries/src/Console/GetConfigurationCommand.php b/libraries/src/Console/GetConfigurationCommand.php
new file mode 100644
index 0000000000000..4c0d1d56c5f80
--- /dev/null
+++ b/libraries/src/Console/GetConfigurationCommand.php
@@ -0,0 +1,372 @@
+ 'db',
+ 'options' => [
+ 'dbtype',
+ 'host',
+ 'user',
+ 'password',
+ 'dbprefix',
+ 'db',
+ 'dbencryption',
+ 'dbsslverifyservercert',
+ 'dbsslkey',
+ 'dbsslcert',
+ 'dbsslca',
+ 'dbsslcipher'
+ ]
+ ];
+
+ /**
+ * Constant defining the Session option group
+ * @var array
+ * @since 4.0
+ */
+ public const SESSION_GROUP = [
+ 'name' => 'session',
+ 'options' => [
+ 'session_handler',
+ 'shared_session',
+ 'session_metadata'
+ ]
+ ];
+
+ /**
+ * Constant defining the Mail option group
+ * @var array
+ * @since 4.0
+ */
+ public const MAIL_GROUP = [
+ 'name' => 'mail',
+ 'options' => [
+ 'mailonline',
+ 'mailer',
+ 'mailfrom',
+ 'fromname',
+ 'sendmail',
+ 'smtpauth',
+ 'smtpuser',
+ 'smtppass',
+ 'smtphost',
+ 'smtpsecure',
+ 'smtpport'
+ ]
+ ];
+
+ /**
+ * Return code if configuration is get successfully
+ * @since 4.0
+ */
+ public const CONFIG_GET_SUCCESSFUL = 0;
+
+ /**
+ * Return code if configuration group option is not found
+ * @since 4.0
+ */
+ public const CONFIG_GET_GROUP_NOT_FOUND = 1;
+
+ /**
+ * Return code if configuration option is not found
+ * @since 4.0
+ */
+ public const CONFIG_GET_OPTION_NOT_FOUND = 2;
+
+ /**
+ * Return code if the command has been invoked with wrong options
+ * @since 4.0
+ */
+ public const CONFIG_GET_OPTION_FAILED = 3;
+
+ /**
+ * Configures the IO
+ *
+ * @param InputInterface $input Console Input
+ * @param OutputInterface $output Console Output
+ *
+ * @return void
+ *
+ * @since 4.0
+ *
+ */
+ private function configureIO(InputInterface $input, OutputInterface $output)
+ {
+ $this->cliInput = $input;
+ $this->ioStyle = new SymfonyStyle($input, $output);
+ }
+
+
+ /**
+ * Displays logically grouped options
+ *
+ * @param string $group The group to be processed
+ *
+ * @return integer
+ *
+ * @since 4.0
+ */
+ public function processGroupOptions($group): int
+ {
+ $configs = $this->getApplication()->getConfig()->toArray();
+ $configs = $this->formatConfig($configs);
+
+ $groups = $this->getGroups();
+
+ $foundGroup = false;
+
+ foreach ($groups as $key => $value)
+ {
+ if ($value['name'] === $group)
+ {
+ $foundGroup = true;
+ $options = [];
+
+ foreach ($value['options'] as $key => $option)
+ {
+ $options[] = [$option, $configs[$option]];
+ }
+
+ $this->ioStyle->table(['Option', 'Value'], $options);
+ }
+ }
+
+ if (!$foundGroup)
+ {
+ $this->ioStyle->error("Group *$group* not found");
+
+ return self::CONFIG_GET_GROUP_NOT_FOUND;
+ }
+
+ return self::CONFIG_GET_SUCCESSFUL;
+ }
+
+ /**
+ * Gets the defined option groups
+ *
+ * @return array
+ *
+ * @since 4.0
+ */
+ public function getGroups()
+ {
+ return [
+ self::DB_GROUP,
+ self::MAIL_GROUP,
+ self::SESSION_GROUP
+ ];
+ }
+
+ /**
+ * Formats the configuration array into desired format
+ *
+ * @param array $configs Array of the configurations
+ *
+ * @return array
+ *
+ * @since 4.0
+ */
+ public function formatConfig(Array $configs): array
+ {
+ $newConfig = [];
+
+ foreach ($configs as $key => $config)
+ {
+ $config = $config === false ? "false" : $config;
+ $config = $config === true ? "true" : $config;
+
+ if (!in_array($key, ['cwd', 'execution']))
+ {
+ $newConfig[$key] = $config;
+ }
+ }
+
+ return $newConfig;
+ }
+
+ /**
+ * Handles the command when a single option is requested
+ *
+ * @param string $option The option we want to get its value
+ *
+ * @return integer
+ *
+ * @since 4.0
+ */
+ public function processSingleOption($option): int
+ {
+ $configs = $this->getApplication()->getConfig()->toArray();
+
+ if (!array_key_exists($option, $configs))
+ {
+ $this->ioStyle->error("Can't find option *$option* in configuration list");
+
+ return self::CONFIG_GET_OPTION_NOT_FOUND;
+ }
+
+ $value = $this->formatConfigValue($this->getApplication()->get($option));
+
+ $this->ioStyle->table(['Option', 'Value'], [[$option, $value]]);
+
+ return self::CONFIG_GET_SUCCESSFUL;
+ }
+
+ /**
+ * Formats the Configuration value
+ *
+ * @param mixed $value Value to be formatted
+ *
+ * @return string
+ *
+ * @since version
+ */
+ protected function formatConfigValue($value): string
+ {
+ if ($value === false)
+ {
+ return 'false';
+ }
+ elseif ($value === true)
+ {
+ return 'true';
+ }
+ elseif ($value === null)
+ {
+ return 'Not Set';
+ }
+ else
+ {
+ return $value;
+ }
+ }
+
+ /**
+ * Initialise the command.
+ *
+ * @return void
+ *
+ * @since 4.0.0
+ */
+ protected function configure(): void
+ {
+ $groups = $this->getGroups();
+
+ foreach ($groups as $key => $group)
+ {
+ $groupNames[] = $group['name'];
+ }
+
+ $groupNames = implode(', ', $groupNames);
+
+ $this->setDescription('Displays the current value of a configuration option');
+
+ $this->addArgument('option', null, 'Name of the option');
+ $this->addOption('group', 'g', InputOption::VALUE_REQUIRED, 'Name of the option');
+
+ $help = "The %command.name% Displays the current value of a configuration option
+ \nUsage: php %command.full_name%