diff --git a/includes/framework.php b/includes/framework.php
index 92102a70c3f05..b33d976cb1cff 100644
--- a/includes/framework.php
+++ b/includes/framework.php
@@ -18,7 +18,16 @@
|| (filesize(JPATH_CONFIGURATION . '/configuration.php') < 10)
|| (file_exists(JPATH_INSTALLATION . '/index.php') && (false === (new Version)->isInDevelopmentState())))
{
- if (file_exists(JPATH_INSTALLATION . '/index.php'))
+ // Prevents the script from falling back to $_SERVER['REQUEST_URI'] as it will throw an error in CLI mode.
+ if (php_sapi_name() === 'cli')
+ {
+ // This is been defined because some core scripts needs it defined.
+ define('JDEBUG', false);
+
+ // We pass control back to the calling script - joomla.php to allow commands like core:install to run
+ return;
+ }
+ elseif (file_exists(JPATH_INSTALLATION . '/index.php'))
{
header('Location: ' . substr($_SERVER['REQUEST_URI'], 0, strpos($_SERVER['REQUEST_URI'], 'index.php')) . 'installation/index.php');
diff --git a/installation/src/Form/Field/Installation/PrefixField.php b/installation/src/Form/Field/Installation/PrefixField.php
index 5d50d75c56d83..3a2889ae2ebd8 100644
--- a/installation/src/Form/Field/Installation/PrefixField.php
+++ b/installation/src/Form/Field/Installation/PrefixField.php
@@ -12,6 +12,7 @@
use Joomla\CMS\Factory;
use Joomla\CMS\Form\FormField;
+use Joomla\CMS\Installation\Helper\DatabaseHelper;
/**
* Database Prefix field.
@@ -45,9 +46,9 @@ protected function getInput()
$disabled = (string) $this->element['disabled'] === 'true' ? ' disabled="disabled"' : '';
// Make sure somebody doesn't put in a too large prefix size value.
- if ($size > 10)
+ if ($size > 15)
{
- $size = 10;
+ $size = 15;
}
// If a prefix is already set, use it instead.
@@ -55,26 +56,7 @@ protected function getInput()
if (empty($session['db_prefix']))
{
- // Create the random prefix.
- $prefix = '';
- $chars = range('a', 'z');
- $numbers = range(0, 9);
-
- // We want the fist character to be a random letter.
- shuffle($chars);
- $prefix .= $chars[0];
-
- // Next we combine the numbers and characters to get the other characters.
- $symbols = array_merge($numbers, $chars);
- shuffle($symbols);
-
- for ($i = 0, $j = $size - 1; $i < $j; ++$i)
- {
- $prefix .= $symbols[$i];
- }
-
- // Add in the underscore.
- $prefix .= '_';
+ $prefix = DatabaseHelper::getPrefix();
}
else
{
diff --git a/installation/src/Helper/DatabaseHelper.php b/installation/src/Helper/DatabaseHelper.php
index d0b4bd49f3aae..7d5c1647e052c 100644
--- a/installation/src/Helper/DatabaseHelper.php
+++ b/installation/src/Helper/DatabaseHelper.php
@@ -69,4 +69,38 @@ public static function getDbo($driver, $host, $user, $password, $database, $pref
return $db;
}
+
+ /**
+ * Generates random prefix string for DB table
+ *
+ * @param int $size Size of the Prefix
+ *
+ * @return string
+ *
+ * @since 4.0
+ */
+ public static function getPrefix(int $size = 15)
+ {
+ // Create the random prefix.
+ $chars = range('a', 'z');
+ $numbers = range(0, 9);
+
+ // We want the first character to be a random letter.
+ shuffle($chars);
+ $prefix = $chars[0];
+
+ // Next we combine the numbers and characters to get the other characters.
+ $symbols = array_merge($numbers, $chars);
+ shuffle($symbols);
+
+ for ($i = 1, $j = $size - 1; $i < $j; ++$i)
+ {
+ $prefix .= $symbols[$i];
+ }
+
+ // Add in the underscore.
+ $prefix .= '_';
+
+ return $prefix;
+ }
}
diff --git a/installation/src/Model/DatabaseModel.php b/installation/src/Model/DatabaseModel.php
index 79cd7eaee7f82..dea93f58884bf 100644
--- a/installation/src/Model/DatabaseModel.php
+++ b/installation/src/Model/DatabaseModel.php
@@ -575,7 +575,7 @@ public function createTables($options)
$serverType = $db->getServerType();
// Set the appropriate schema script based on UTF-8 support.
- $schema = 'sql/' . $serverType . '/joomla.sql';
+ $schema = JPATH_INSTALLATION . '/sql/' . $serverType . '/joomla.sql';
// Check if the schema is a valid file
if (!is_file($schema))
@@ -700,7 +700,7 @@ public function createTables($options)
}
// Load the localise.sql for translating the data in joomla.sql.
- $dblocalise = 'sql/' . $serverType . '/localise.sql';
+ $dblocalise = JPATH_INSTALLATION . '/sql/' . $serverType . '/localise.sql';
if (is_file($dblocalise))
{
@@ -722,7 +722,7 @@ public function createTables($options)
}
// Handle default backend language setting. This feature is available for localized versions of Joomla.
- $languages = Factory::getApplication()->getLocaliseAdmin($db);
+ $languages = $this->getLocaliseAdmin($db);
if (in_array($options->language, $languages['admin']) || in_array($options->language, $languages['site']))
{
@@ -767,6 +767,109 @@ public function createTables($options)
return $return;
}
+ /**
+ * Returns the installed language in the administrative and frontend area.
+ *
+ * @param DatabaseInterface $db Database driver.
+ *
+ * @return array Array with installed language packs in admin and site area.
+ *
+ * @since 4.0
+ */
+ public function getLocaliseAdmin(DatabaseInterface $db = null)
+ {
+ $langfiles = [];
+
+ // If db connection, fetch them from the database.
+ if ($db)
+ {
+ foreach (LanguageHelper::getInstalledLanguages() as $clientId => $languages)
+ {
+ $clientName = $clientId === 0 ? 'site' : 'admin';
+
+ foreach ($languages as $language)
+ {
+ $langfiles[$clientName][] = $language->element;
+ }
+ }
+
+ return $langfiles;
+ }
+
+ // Read the folder names in the site and admin area.
+ return [
+ 'site' => Folder::folders(LanguageHelper::getLanguagePath(JPATH_SITE)),
+ 'admin' => Folder::folders(LanguageHelper::getLanguagePath(JPATH_ADMINISTRATOR)),
+ ];
+ }
+
+ /**
+ * Method to install the sample data.
+ *
+ * @return boolean True on success.
+ *
+ * @since 3.1
+ */
+ public function installSampleData()
+ {
+ $db = Factory::getDbo();
+
+ // Build the path to the sample data file.
+ $type = $db->getServerType();
+
+ if (Factory::getApplication()->input->get('sample_file', ''))
+ {
+ $sample_file = Factory::getApplication()->input->get('sample_file', '');
+ }
+ else
+ {
+ $sample_file = 'sample_testing.sql';
+ }
+
+ $data = JPATH_INSTALLATION . '/sql/' . $type . '/' . $sample_file;
+
+ // Attempt to import the database schema if one is chosen.
+ if ($sample_file != '')
+ {
+ if (!file_exists($data))
+ {
+ Factory::getApplication()->enqueueMessage(Text::sprintf('INSTL_DATABASE_FILE_DOES_NOT_EXIST', $data), 'error');
+
+ return false;
+ }
+ elseif (!$this->populateDatabase($db, $data))
+ {
+ return false;
+ }
+
+ $this->postInstallSampleData($db, $sample_file);
+ }
+
+ return true;
+ }
+
+ /**
+ * Sample data tables and data post install process.
+ *
+ * @param \JDatabaseDriver $db Database connector object $db*.
+ * @param string $sampleFileName The sample dats filename.
+ *
+ * @return void
+ *
+ * @since 3.1
+ */
+ protected function postInstallSampleData($db, $sampleFileName = '')
+ {
+ // Update the sample data user ids.
+ $this->updateUserIds($db);
+
+ // If not joomla sample data for testing, update the sample data dates.
+ if ($sampleFileName !== 'sample_testing.sql')
+ {
+ $this->updateDates($db);
+ }
+ }
+
/**
* Method to install the cms data.
*
diff --git a/libraries/bootstrap.php b/libraries/bootstrap.php
index 705d690054c38..ab02c7a98b1cc 100644
--- a/libraries/bootstrap.php
+++ b/libraries/bootstrap.php
@@ -83,3 +83,9 @@ class_exists('\\Joomla\\CMS\\Autoload\\ClassLoader');
// Register the PasswordHash library.
JLoader::register('PasswordHash', JPATH_PLATFORM . '/phpass/PasswordHash.php');
+
+// Registers the Installation namespace
+if (file_exists(JPATH_INSTALLATION . '/index.php'))
+{
+ JLoader::registerNamespace('Joomla\\CMS\\Installation', JPATH_INSTALLATION . '/src', false, false, 'psr4');
+}
diff --git a/libraries/src/Application/ConsoleApplication.php b/libraries/src/Application/ConsoleApplication.php
index cf11df8894e1a..6bed5650664a4 100644
--- a/libraries/src/Application/ConsoleApplication.php
+++ b/libraries/src/Application/ConsoleApplication.php
@@ -14,6 +14,7 @@
use Joomla\CMS\Extension\ExtensionManagerTrait;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\PluginHelper;
+use Joomla\CMS\Version;
use Joomla\Console\Application;
use Joomla\DI\Container;
use Joomla\DI\ContainerAwareTrait;
@@ -22,6 +23,8 @@
use Joomla\Event\DispatcherInterface;
use Joomla\Registry\Registry;
use Joomla\Session\SessionInterface;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
/**
@@ -57,6 +60,14 @@ class ConsoleApplication extends Application implements DispatcherAwareInterface
*/
private $session;
+ /**
+ * The client identifier.
+ *
+ * @var integer
+ * @since 4.0
+ */
+ protected $clientId = 4;
+
/**
* Class constructor.
*
@@ -163,8 +174,11 @@ public function execute()
$this->createExtensionNamespaceMap();
// Import CMS plugin groups to be able to subscribe to events
- PluginHelper::importPlugin('system');
- PluginHelper::importPlugin('console');
+ if (file_exists(JPATH_CONFIGURATION . '/configuration.php'))
+ {
+ PluginHelper::importPlugin('system');
+ PluginHelper::importPlugin('console');
+ }
parent::execute();
}
@@ -307,6 +321,31 @@ public function setSession(SessionInterface $session): self
return $this;
}
+
+ /**
+ * Flush the media version to refresh versionable assets
+ *
+ * @return void
+ *
+ * @since 4.0
+ */
+ public function flushAssets()
+ {
+ (new Version)->refreshMediaVersion();
+ }
+
+ /**
+ * Gets the client id of the current running application.
+ *
+ * @return integer A client identifier.
+ *
+ * @since 4.0
+ */
+ public function getClientId()
+ {
+ return $this->clientId;
+ }
+
/**
* Returns the application \JMenu object.
*
diff --git a/libraries/src/Console/CheckJoomlaUpdatesCommand.php b/libraries/src/Console/CheckJoomlaUpdatesCommand.php
new file mode 100644
index 0000000000000..1131d5d866ae6
--- /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)
+ {
+ 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/CoreInstallCommand.php b/libraries/src/Console/CoreInstallCommand.php
new file mode 100644
index 0000000000000..59e42602e0419
--- /dev/null
+++ b/libraries/src/Console/CoreInstallCommand.php
@@ -0,0 +1,784 @@
+load('', JPATH_INSTALLATION, null, false, false) ||
+ $language->load('', JPATH_INSTALLATION, null, true);
+
+ $this->registry = new Registry;
+ $this->cliInput = $input;
+
+ ProgressBar::setFormatDefinition('custom', ' %current%/%max% -- %message%');
+ $this->progressBar = new ProgressBar($output, 7);
+ $this->progressBar->setFormat('custom');
+
+ $this->ioStyle = new SymfonyStyle($input, $output);
+ }
+
+
+ /**
+ * Verifies database connection
+ *
+ * @param array $options Options array
+ *
+ * @return boolean|\Joomla\Database\DatabaseInterface
+ *
+ * @since 4.0
+ * @throws \Exception
+ */
+ public function checkDatabaseConnection($options)
+ {
+ // Get the options as an object for easier handling.
+ $options = ArrayHelper::toObject($options);
+
+ // Load the backend language files so that the DB error messages work.
+ $lang = Factory::getLanguage();
+ $currentLang = $lang->getTag();
+
+ // Load the selected language
+ if (LanguageHelper::exists($currentLang, JPATH_ADMINISTRATOR))
+ {
+ $lang->load('joomla', JPATH_ADMINISTRATOR, $currentLang, true);
+ }
+ // Pre-load en-GB in case the chosen language files do not exist.
+ else
+ {
+ $lang->load('joomla', JPATH_ADMINISTRATOR, 'en-GB', true);
+ }
+
+ // Ensure a database type was selected.
+ if (empty($options->db_type))
+ {
+ $this->getApplication()->enqueueMessage(Text::_('INSTL_DATABASE_INVALID_TYPE'), 'warning');
+
+ return false;
+ }
+
+ // Ensure that a hostname and user name were input.
+ if (empty($options->db_host) || empty($options->db_user))
+ {
+ $this->getApplication()->enqueueMessage(Text::_('INSTL_DATABASE_INVALID_DB_DETAILS'), 'warning');
+
+ return false;
+ }
+
+ // Ensure that a database name was input.
+ if (empty($options->db_name))
+ {
+ $this->getApplication()->enqueueMessage(Text::_('INSTL_DATABASE_EMPTY_NAME'), 'warning');
+
+ return false;
+ }
+
+ // Validate database table prefix.
+ if (isset($options->db_prefix) && !preg_match('#^[a-zA-Z]+[a-zA-Z0-9_]*$#', $options->db_prefix))
+ {
+ $this->getApplication()->enqueueMessage(Text::_('INSTL_DATABASE_PREFIX_MSG'), 'warning');
+
+ return false;
+ }
+
+ // Validate length of database table prefix.
+ if (isset($options->db_prefix) && strlen($options->db_prefix) > 15)
+ {
+ $this->getApplication()->enqueueMessage(Text::_('INSTL_DATABASE_FIX_TOO_LONG'), 'warning');
+
+ return false;
+ }
+
+ // Validate length of database name.
+ if (strlen($options->db_name) > 64)
+ {
+ $this->getApplication()->enqueueMessage(Text::_('INSTL_DATABASE_NAME_TOO_LONG'), 'warning');
+
+ return false;
+ }
+
+ // Workaround for UPPERCASE table prefix for PostgreSQL
+ if (in_array($options->db_type, ['pgsql', 'postgresql']))
+ {
+ if (isset($options->db_prefix) && strtolower($options->db_prefix) !== $options->db_prefix)
+ {
+ $this->getApplication()->enqueueMessage(Text::_('INSTL_DATABASE_FIX_LOWERCASE'), 'warning');
+
+ return false;
+ }
+ }
+
+ // Build the connection options array.
+ $settings = [
+ 'driver' => $options->db_type,
+ 'host' => $options->db_host,
+ 'user' => $options->db_user,
+ 'password' => $options->db_pass_plain,
+ 'database' => $options->db_name,
+ 'prefix' => $options->db_prefix,
+ 'select' => isset($options->db_select) ? $options->db_select : false
+ ];
+
+ try
+ {
+ return DatabaseDriver::getInstance($settings)->connect() !== false;
+ }
+ catch (\RuntimeException $e)
+ {
+ $this->getApplication()->enqueueMessage(
+ Text::sprintf(
+ 'Check your database credentials, database type, database name or hostname.
+ If you have MySQL 8 installed then please read
+ https://docs.joomla.org/Joomla_and_MySQL_8#Workaround_to_get_Joomla_working_with_MySQL_8
+ for more information.',
+ null
+ ),
+ 'error'
+ );
+
+ return false;
+ }
+ }
+
+ /**
+ * Handles non-interactive installation
+ *
+ * @param string $file Path to installation
+ * @param boolean $validate Option to validate the data or not
+ *
+ * @since 4.0
+ *
+ * @return array | null
+ */
+ public function processNonInteractiveInstallation($file, $validate = true)
+ {
+ if (!File::exists($file))
+ {
+ $this->getApplication()->enqueueMessage('Unable to locate the specified file', 'error');
+
+ return null;
+ }
+
+ $allowedExtension = ['json', 'ini'];
+ $ext = File::getExt($file);
+
+ if (!in_array($ext, $allowedExtension))
+ {
+ $this->getApplication()->enqueueMessage('The file type specified is not supported');
+
+ return null;
+ }
+
+ $options = $this->registry->loadFile($file, $ext)->toArray();
+ $optionalKeys = ['language', 'helpurl', 'db_old', 'db_prefix'];
+ $requiredKeys = array_diff(array_keys($this->getDefaultOptions()), $optionalKeys);
+ $providedKeys = array_diff(array_keys($options), $optionalKeys);
+ sort($requiredKeys);
+ sort($providedKeys);
+
+ if ($requiredKeys != $providedKeys)
+ {
+ $diff = array_diff($requiredKeys, $providedKeys);
+ $remainingKeys = implode(', ', $diff);
+ $this->ioStyle->error("These options are required in your file: [$remainingKeys]");
+
+ return null;
+ }
+
+ array_walk(
+ $optionalKeys, function ($value, $key) use (&$options) {
+ if (!isset($options[$value]))
+ {
+ switch ($value)
+ {
+ case 'db_prefix':
+ $options[$value] = (new PrefixField)->getPrefix();
+ break;
+ case 'db_old':
+ $options[$value] = 'backup';
+ break;
+ default:
+ $options[$value] = '';
+ break;
+ }
+ }
+ }
+ );
+
+ if ($validate)
+ {
+ $validator = $this->validate($options);
+
+ return $validator ? $options : null;
+ }
+
+ return $options;
+ }
+
+ /**
+ * Display enqueued messages by application
+ *
+ * @since 4.0
+ *
+ * @return void
+ */
+ public function outputEnqueuedMessages()
+ {
+ $messages = $this->getApplication()->getMessageQueue();
+
+ foreach ($messages as $k => $message)
+ {
+ $this->displayMessage($message[0]);
+ }
+ }
+
+ /**
+ * Parse an INI file
+ *
+ * @param string $file Path to ini file
+ *
+ * @return array
+ *
+ * @since 4.0
+ */
+ public function parseIniFile($file)
+ {
+ $disabledFunctions = explode(',', ini_get('disable_functions'));
+ $isParseIniFileDisabled = in_array('parse_ini_file', array_map('trim', $disabledFunctions));
+
+ if (!function_exists('parse_ini_file') || $isParseIniFileDisabled)
+ {
+ $contents = file_get_contents($file);
+ $contents = str_replace('"_QQ_"', '\\"', $contents);
+ $options = @parse_ini_string($contents, INI_SCANNER_RAW);
+ }
+ else
+ {
+ $options = @parse_ini_file($file);
+ }
+
+ if (!is_array($options))
+ {
+ $options = array();
+ }
+
+ return $options;
+ }
+
+ /**
+ * Performs environment checks before installation
+ *
+ * @return boolean
+ *
+ * @since 4.0
+ */
+ public function runChecks()
+ {
+ $pass = $this->check->getPhpOptionsSufficient();
+
+ if ($pass)
+ {
+ return true;
+ }
+
+ $phpoptions = $this->check->getPhpOptions();
+
+ foreach ($phpoptions as $option)
+ {
+ $option->notice = $option->notice ? $option->notice : "OK";
+ $options[] = (array) $option;
+ }
+
+ $this->envOptions = $options;
+
+ return false;
+ }
+
+ /**
+ * Initialise the command.
+ *
+ * @return void
+ *
+ * @since 4.0.0
+ */
+ protected function configure(): void
+ {
+ $this->setDescription('Sets up the Joomla! CMS.');
+
+ $this->addOption('file', 'f', InputOption::VALUE_REQUIRED, 'Type of the extension');
+
+ $help = <<<'EOF'
+The %command.name% is used for setting up the Joomla! CMS
+
+ php %command.full_name%
+
+To set up Joomla! using an existing configuration file, use the --file option. This may be either a JSON or INI file.
+
+ php %command.full_name% --file=
+EOF;
+ $this->setHelp($help);
+ }
+
+
+ /**
+ * Retrieves options Template
+ *
+ * @return array
+ *
+ * @since 4.0
+ */
+ public function getOptionsTemplate()
+ {
+ $drivers = array_map('strtolower', DatabaseDriver::getConnectors());
+ $prefix = DatabaseHelper::getPrefix(8);
+
+ return [
+ 'language' => [
+ 'question' => "Site Language",
+ 'type' => 'select',
+ 'optionData' => ['en-GB', 'en-US'],
+ 'default' => 'en-GB',
+ ],
+ 'site_name' => [
+ 'question' => "What's the name of your website",
+ 'type' => 'question',
+ ],
+ 'admin_email' => [
+ 'question' => "Enter admin email",
+ 'type' => 'question',
+ 'rules' => 'isEmail',
+ ],
+ 'admin_user' => [
+ 'question' => "Enter admin username",
+ 'type' => 'question',
+ 'rules' => 'isAlphanumeric',
+ ],
+ 'admin_password_plain' => [
+ 'question' => "Enter admin password",
+ 'type' => 'question',
+ ],
+ 'db_type' => [
+ 'question' => "Select your connection type",
+ 'type' => 'select',
+ 'optionData' => $drivers,
+ 'default' => 'mysqli',
+ ],
+ 'db_host' => [
+ 'question' => "Enter database host",
+ 'type' => 'question',
+ ],
+ 'db_user' => [
+ 'question' => "Enter database user",
+ 'type' => 'question',
+ ],
+ 'db_pass_plain' => [
+ 'question' => "Enter database password",
+ 'type' => 'question',
+ 'default' => null,
+ ],
+ 'db_name' => [
+ 'question' => "Enter database name",
+ 'type' => 'question',
+ ],
+ 'db_prefix' => [
+ 'question' => "Database prefix",
+ 'type' => 'question',
+ 'default' => $prefix,
+ ],
+ 'db_old' => [
+ 'question' => "Remove or backup old database",
+ 'type' => 'select',
+ 'optionData' => ['remove', 'backup'],
+ 'default' => 'backup',
+ ],
+ 'helpurl' => [
+ 'question' => "Help URL",
+ 'type' => 'question',
+ 'default' => '',
+ ],
+ ];
+ }
+
+ /**
+ * Defines default options
+ *
+ * @return array
+ *
+ * @since 4.0
+ */
+ public function getDefaultOptions()
+ {
+ return [
+ 'language' => 'en-GB',
+ 'site_name' => 'Joomla',
+ 'admin_email' => 'email@example.com',
+ 'admin_user' => 'user',
+ 'admin_password' => 'password',
+ 'admin_password_plain' => 'password',
+ 'db_type' => 'Mysql',
+ 'db_host' => 'localhost',
+ 'db_user' => 'root',
+ 'db_pass_plain' => '',
+ 'db_name' => 'test',
+ 'db_prefix' => 'prefix_',
+ 'db_old' => 'remove',
+ 'helpurl' => '',
+ ];
+ }
+
+ /**
+ * Retrieves options from user inputs
+ *
+ * @return array
+ *
+ * @since 4.0
+ */
+ private function collectOptions()
+ {
+ $data = $this->getOptionsTemplate();
+
+ $options = $this->getDefaultOptions();
+
+ foreach ($data as $key => $value)
+ {
+ $valid = false;
+
+ while (!$valid)
+ {
+ $val = $this->processType($value);
+ $options[$key] = $val;
+
+ $validator = $this->validate($options);
+
+ if (!$validator)
+ {
+ $this->outputEnqueuedMessages();
+ }
+ else
+ {
+ $valid = true;
+ }
+ }
+ }
+
+ return $options;
+ }
+
+ /**
+ * Displays an error Message
+ *
+ * @param string $message Message to be displayed
+ *
+ * @return void
+ *
+ * @since 4.0
+ */
+ public function displayMessage($message)
+ {
+ $this->ioStyle->error(Text::_($message));
+ }
+
+ /**
+ * Process a console input type
+ *
+ * @param array $data The option template
+ *
+ * @return mixed
+ *
+ * @since 4.0
+ */
+ private function processType($data)
+ {
+ $default = $data['default'] ?? null;
+
+ switch ($data['type'])
+ {
+ case 'question':
+ $placeholder = \uniqid("placeholder");
+ $value = $this->ioStyle->ask(
+ $data['question'],
+ $default,
+ function ($string) use ($placeholder) {
+ return (null == $string) ? $placeholder : $string;
+ }
+ );
+
+ return str_replace($placeholder, null, $value);
+ break;
+
+ case 'select':
+ return $this->ioStyle->choice($data['question'], $data['optionData'], $default);
+ break;
+ }
+ }
+
+ /**
+ * Validates the given Data
+ *
+ * @param array $data Data to be validated
+ *
+ * @return array | boolean
+ *
+ * @since 4.0
+ */
+ public function validate($data)
+ {
+ return $this->setup->validate($data);
+ }
+
+ /**
+ * 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__
+ * @throws \Exception
+ */
+ protected function doExecute(InputInterface $input, OutputInterface $output): int
+ {
+ $this->configureIO($input, $output);
+ $this->progressBar->setMessage("Starting Joomla! installation ...");
+ $this->progressBar->start();
+ define('JPATH_COMPONENT', JPATH_BASE . '/installation');
+
+ if (file_exists(JPATH_CONFIGURATION . '/configuration.php'))
+ {
+ $this->progressBar->finish();
+ $this->ioStyle->warning("Joomla! is already installed and set up.");
+
+ return self::JOOMLA_ALREADY_SETUP;
+ }
+
+ if (!Folder::exists(JPATH_INSTALLATION))
+ {
+ $this->ioStyle->warning("Installation directory cannot be found.");
+
+ return self::INSTALLATION_DIRECTORY_NOT_FOUND;
+ }
+
+ $this->progressBar->advance();
+ $this->setup = new SetupModel;
+ $this->check = new ChecksModel;
+
+ $this->progressBar->setMessage("Running checks ...");
+ $passed = $this->runChecks();
+
+ if (!$passed)
+ {
+ $this->progressBar->finish();
+ $this->ioStyle->warning('These settings are recommended for PHP to ensure full compatibility with Joomla.');
+ $this->ioStyle->table(['Label', 'State', 'Notice'], $this->envOptions);
+
+ return self::PHP_OPTIONS_NOT_SET;
+ }
+
+ $this->progressBar->advance();
+ $file = $this->cliInput->getOption('file');
+
+ if ($file)
+ {
+ $this->progressBar->setMessage("Loading file ...");
+ $result = $this->processNonInteractiveInstallation($file);
+
+ if (!is_array($result))
+ {
+ $this->progressBar->finish();
+
+ return self::BAD_INPUT_FILE;
+ }
+
+ $this->progressBar->setMessage("File loaded");
+ $this->progressBar->advance();
+ $options = $result;
+ }
+ else
+ {
+ $this->progressBar->setMessage("Collecting options ...");
+ $options = $this->collectOptions();
+ }
+
+ $this->progressBar->setMessage("Checking database connection ...");
+ $this->progressBar->advance();
+ $validConnection = $this->checkDatabaseConnection($options);
+ $this->progressBar->advance();
+
+ if ($validConnection)
+ {
+ $model = new ConfigurationModel;
+
+ $this->progressBar->setMessage("Writing configuration ...");
+ $this->getApplication()->getSession()->set('setup.options', $options);
+
+ $this->getApplication()->getSession()->set('setup.options', $options);
+ $completed = $model->setup($options);
+ $this->progressBar->advance();
+
+ if ($completed)
+ {
+ $this->progressBar->setMessage("Finishing installation ..." . PHP_EOL);
+ $this->progressBar->finish();
+ $this->ioStyle->success("Joomla! installation completed successfully!");
+
+ return self::INSTALLATION_SUCCESSFUL;
+ }
+
+ $this->progressBar->finish();
+ $this->ioStyle->error("Joomla! installation was unsuccessful!");
+
+ return self::INSTALLATION_UNSUCCESSFUL;
+ }
+
+ $this->progressBar->finish();
+
+ return self::INSTALLATION_UNSUCCESSFUL;
+ }
+}
diff --git a/libraries/src/Console/ExtensionInstallCommand.php b/libraries/src/Console/ExtensionInstallCommand.php
new file mode 100644
index 0000000000000..8529d60078471
--- /dev/null
+++ b/libraries/src/Console/ExtensionInstallCommand.php
@@ -0,0 +1,235 @@
+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|integer
+ *
+ * @since 4.0
+ *
+ * @throws \Exception
+ */
+ public function processPathInstallation($path)
+ {
+ if (!file_exists($path))
+ {
+ $this->ioStyle->warning('The file path specified does not exist.');
+
+ return false;
+ }
+
+ $tmp_path = $this->getApplication()->get('tmp_path');
+ $tmp_path = $tmp_path . '/' . basename($path);
+ $package = InstallerHelper::unpack($path, true);
+
+ if ($package['type'] === false)
+ {
+ return false;
+ }
+
+ $jInstaller = Installer::getInstance();
+ $result = $jInstaller->install($package['extractdir']);
+ InstallerHelper::cleanupInstall($tmp_path, $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)
+ {
+ $filename = InstallerHelper::downloadPackage($url);
+
+ $tmp_path = $this->getApplication()->get('tmp_path');
+
+ $path = $tmp_path . '/' . 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;
+ }
+ else
+ {
+ $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;
+ }
+ else
+ {
+ $this->ioStyle->success('Extension installed successfully.');
+
+ return self::INSTALLATION_SUCCESSFUL;
+ }
+ }
+ else
+ {
+ $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..865c309a0c3a2
--- /dev/null
+++ b/libraries/src/Console/ExtensionRemoveCommand.php
@@ -0,0 +1,156 @@
+cliInput = $input;
+ $this->ioStyle = new SymfonyStyle($input, $output);
+ }
+
+ /**
+ * Initialise the command.
+ *
+ * @return void
+ *
+ * @since 4.0.0
+ */
+ protected function configure(): void
+ {
+ $this->addArgument(
+ 'extension_id',
+ 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);
+ }
+
+ /**
+ * Gets the extension from DB
+ *
+ * @return boolean
+ *
+ * @since 4.0
+ */
+ protected function getExtension()
+ {
+ return Table::getInstance('extension');
+ }
+
+ /**
+ * 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');
+
+ $extension = $this->getExtension();
+
+ if ((int) $extensionId === 0 || !$extension->load($extensionId))
+ {
+ $this->ioStyle->error("Extension with ID of $extensionId not found.");
+
+ return 0;
+ }
+
+ $response = $this->ioStyle->ask('Are you sure you want to remove this extension?', 'yes/no');
+
+ if (strtolower($response) === 'yes')
+ {
+ if ($extension->type && $extension->type != 'language')
+ {
+ $installer = Installer::getInstance();
+ $result = $installer->uninstall($extension->type, $extensionId);
+
+ if ($result)
+ {
+ $this->ioStyle->success('Extension removed!');
+ }
+ }
+ }
+ elseif (strtolower($response) === 'no')
+ {
+ $this->ioStyle->note('Extension not removed.');
+
+ return 0;
+ }
+ else
+ {
+ $this->ioStyle->warning('Invalid response');
+
+ return 2;
+ }
+ }
+}
diff --git a/libraries/src/Console/ExtensionsListCommand.php b/libraries/src/Console/ExtensionsListCommand.php
new file mode 100644
index 0000000000000..86f957dc21216
--- /dev/null
+++ b/libraries/src/Console/ExtensionsListCommand.php
@@ -0,0 +1,242 @@
+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)
+ {
+ if (!$extensions)
+ {
+ $this->extensions = $this->getAllExtensionsFromDB();
+ }
+ else
+ {
+ $this->extensions = $extensions;
+ }
+ }
+
+ /**
+ * Retrieves extension list from DB
+ *
+ * @return array
+ *
+ * @since 4.0
+ */
+ private function getAllExtensionsFromDB()
+ {
+ $db = Factory::getDbo();
+ $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)
+ {
+ $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)
+ {
+ $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..bc83f9353db6d
--- /dev/null
+++ b/libraries/src/Console/GetConfigurationCommand.php
@@ -0,0 +1,317 @@
+ 'db', 'options' => ['dbtype', 'host', 'user', 'password', 'dbprefix', 'db']];
+
+ /**
+ * Constant defining the Session option group
+ * @var array
+ * @since 4.0
+ */
+ const SESSION_GROUP = ['name' => 'session', 'options' => ['session_handler', 'shared_session', 'session_metadata']];
+
+ /**
+ * Constant defining the Mail option group
+ * @var array
+ * @since 4.0
+ */
+ const MAIL_GROUP = [
+ 'name' => 'mail',
+ 'options' => [
+ 'mailonline', 'mailer', 'mailfrom',
+ 'fromname', 'sendmail', 'smtpauth',
+ 'smtpuser', 'smtppass', 'smtphost',
+ 'smtpsecure', 'smtpport'
+ ]
+ ];
+
+ /**
+ * 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)
+ {
+ $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");
+ exit;
+ }
+
+ return 0;
+ }
+
+ /**
+ * 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($configs)
+ {
+ $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)
+ {
+ $configs = $this->getApplication()->getConfig()->toArray();
+
+ if (!array_key_exists($option, $configs))
+ {
+ $this->ioStyle->error("Can't find option *$option* in configuration list");
+
+ return 1;
+ }
+
+ $value = $this->formatConfigValue($this->getApplication()->get($option));
+
+ $this->ioStyle->table(['Option', 'Value'], [[$option, $value]]);
+
+ return 0;
+ }
+
+ /**
+ * Formats the Configuration value
+ *
+ * @param mixed $value Value to be formatted
+ *
+ * @return string
+ *
+ * @since version
+ */
+ protected function formatConfigValue($value)
+ {
+ 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%