diff --git a/.drone.yml b/.drone.yml
index d5f2fae702d..ea8a71e873a 100644
--- a/.drone.yml
+++ b/.drone.yml
@@ -217,7 +217,7 @@ steps:
environment:
JOOMLA_INSTALLATION_DISABLE_LOCALHOST_CHECK: 1
commands:
- - bash tests/Codeception/drone-api-run.sh "$(pwd)" mysql
+ - bash tests/Codeception/drone-api-run.sh "$(pwd)" mysql mysqli mysql jos_
- name: phpmax-api-mysql
depends_on:
@@ -226,7 +226,7 @@ steps:
environment:
JOOMLA_INSTALLATION_DISABLE_LOCALHOST_CHECK: 1
commands:
- - bash tests/Codeception/drone-api-run.sh "$(pwd)" mysqlphpmax
+ - bash tests/Codeception/drone-api-run.sh "$(pwd)" mysqlphpmax mysqli mysql phpmax_
# - name: phpnext-api-mysql
# depends_on:
@@ -236,7 +236,7 @@ steps:
# environment:
# JOOMLA_INSTALLATION_DISABLE_LOCALHOST_CHECK: 1
# commands:
-# - bash tests/Codeception/drone-api-run.sh "$(pwd)" mysqlphpnext
+# - bash tests/Codeception/drone-api-run.sh "$(pwd)" mysqlphpnext mysqli mysql8 phpmax_
- name: phpmin-api-postgres
depends_on:
@@ -245,7 +245,7 @@ steps:
environment:
JOOMLA_INSTALLATION_DISABLE_LOCALHOST_CHECK: 1
commands:
- - bash tests/Codeception/drone-api-run.sh "$(pwd)" postgres
+ - bash tests/Codeception/drone-api-run.sh "$(pwd)" postgres pgsql postgres jos_
- name: phpmax-api-postgres
depends_on:
@@ -254,7 +254,7 @@ steps:
environment:
JOOMLA_INSTALLATION_DISABLE_LOCALHOST_CHECK: 1
commands:
- - bash tests/Codeception/drone-api-run.sh "$(pwd)" postgresphpmax
+ - bash tests/Codeception/drone-api-run.sh "$(pwd)" postgresphpmax pgsql postgres phpmax_
# - name: phpnext-api-postgres
# depends_on:
@@ -264,7 +264,7 @@ steps:
# environment:
# JOOMLA_INSTALLATION_DISABLE_LOCALHOST_CHECK: 1
# commands:
-# - bash tests/Codeception/drone-api-run.sh "$(pwd)" postgresphpnext
+# - bash tests/Codeception/drone-api-run.sh "$(pwd)" postgresphpnext pgsql postgres
- name: phpmin-system-mysql
depends_on:
@@ -524,6 +524,6 @@ trigger:
---
kind: signature
-hmac: f496f213481b3afcec73da651fc838af21f76950954daac599e665ceb382cc2d
+hmac: 35f71c2c3f977f0da835b27f18cea8f07419c3dd14aae4bc8da11fdb624983c5
...
diff --git a/installation/INSTALL b/installation/INSTALL
index 1badcf67b1b..57bb0fa8588 100644
--- a/installation/INSTALL
+++ b/installation/INSTALL
@@ -109,20 +109,31 @@ INSTALLATION
and then enter 'exit' or 'quit' to exit MySQL.
-3. WEB INSTALLER
+3. INSTALLATION
+ The main method of installation is via the web browser. You can start that
+ installation by simply pointing your web browser to http://www.example.org
+ where the Joomla! web based installer will guide you through the rest of
+ the installation.
+
+ An alternative method of installation is from the command line. On the command line of your
+ server, in the root folder of Joomla, you can run the following command
+
+ php installation/joomla.php install
+
+ You will be guided through the rest of the installation and ou can get further help by running
+
+ php installation/joomla.php help install
-Finally point your web browser to http://www.example.org where the Joomla! web
-based installer will guide you through the rest of the installation.
4. CONFIGURE Joomla
-You can now launch your browser and point it to your Joomla! site eg
+ You can now launch your browser and point it to your Joomla! site eg
- http://www.example.org -> Main Site
- http://www.example.org/administrator -> Admin
+ http://www.example.org -> Main Site
+ http://www.example.org/administrator -> Admin
-You can log into Admin using the username and password that you chose
-during the web based install.
+ You can log into Admin using the username and password that you chose
+ during the install.
Joomla! ADMINISTRATION
diff --git a/installation/forms/setup.xml b/installation/forms/setup.xml
index 1903910c5b9..f8a162b4326 100644
--- a/installation/forms/setup.xml
+++ b/installation/forms/setup.xml
@@ -15,14 +15,6 @@
class="form-control"
required="true"
/>
-
+
+ * @license GNU General Public License version 2 or later; see LICENSE.txt
+ */
+
+defined('_JEXEC') or die;
+
+// Define the base path and require the other defines
+define('JPATH_BASE', dirname(__DIR__));
+
+require_once __DIR__ . '/defines.php';
+
+// Check for presence of vendor dependencies not included in the git repository
+if (!file_exists(JPATH_LIBRARIES . '/vendor/autoload.php') || !is_dir(JPATH_ROOT . '/media/vendor')) {
+ echo 'It looks like you are trying to run Joomla! from our git repository.' . PHP_EOL;
+ echo 'To do so requires you complete a couple of extra steps first.' . PHP_EOL;
+ echo 'Please see https://docs.joomla.org/Special:MyLanguage/J4.x:Setting_Up_Your_Local_Environment for further details.' . PHP_EOL;
+
+ exit;
+}
+
+// Get the framework.
+require_once __DIR__ . '/framework.php';
+
+// Check if the default log directory can be written to, add a logger for errors to use it
+if (is_writable(JPATH_ADMINISTRATOR . '/logs')) {
+ \Joomla\CMS\Log\Log::addLogger(
+ [
+ 'format' => '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}',
+ 'text_file' => 'error.php'
+ ],
+ \Joomla\CMS\Log\Log::ALL,
+ ['error']
+ );
+}
+
+// Register the Installation application
+JLoader::registerNamespace('Joomla\\CMS\\Installation', JPATH_INSTALLATION . '/src', false, false);
+
+// Get the dependency injection container
+$container = \Joomla\CMS\Factory::getContainer();
+$container->registerServiceProvider(new \Joomla\CMS\Installation\Service\Provider\Application());
+
+/*
+ * Alias the session service keys to the CLI session service as that is the primary session backend for this application
+ *
+ * In addition to aliasing "common" service keys, we also create aliases for the PHP classes to ensure autowiring objects
+ * is supported. This includes aliases for aliased class names, and the keys for aliased class names should be considered
+ * deprecated to be removed when the class name alias is removed as well.
+ */
+$container->alias('session', 'session.cli')
+ ->alias('JSession', 'session.cli')
+ ->alias(\Joomla\CMS\Session\Session::class, 'session.cli')
+ ->alias(\Joomla\Session\Session::class, 'session.cli')
+ ->alias(\Joomla\Session\SessionInterface::class, 'session.cli');
+
+// Instantiate and execute the application
+$container->get(\Joomla\CMS\Installation\Application\CliInstallationApplication::class)->execute();
diff --git a/installation/joomla.php b/installation/joomla.php
new file mode 100644
index 00000000000..08893f79d0b
--- /dev/null
+++ b/installation/joomla.php
@@ -0,0 +1,39 @@
+
+ * @license GNU General Public License version 2 or later; see LICENSE.txt
+ */
+
+/**
+ * NOTE: This file should remain compatible with PHP 5.2 to allow us to run our PHP minimum check and show a friendly error message
+ */
+
+/**
+ * Define the application's minimum supported PHP version as a constant so it can be referenced within the application.
+ */
+define('JOOMLA_MINIMUM_PHP', '7.2.5');
+
+if (version_compare(PHP_VERSION, JOOMLA_MINIMUM_PHP, '<')) {
+ echo 'Sorry, your PHP version is not supported.' . PHP_EOL;
+ echo 'Your command line php needs to be version ' . JOOMLA_MINIMUM_PHP . ' or newer to run the Joomla! CLI Tools' . PHP_EOL;
+ echo 'The version of PHP currently running this code, at the command line, is PHP version ' . PHP_VERSION . '.' . PHP_EOL;
+ echo 'Please note, the version of PHP running your commands here, may be different to the version that is used by ';
+ echo 'your web server to run the Joomla! Web Application' . PHP_EOL;
+
+ exit;
+}
+
+/**
+ * Constant that is checked in included files to prevent direct access.
+ * define() is used rather than "const" to not error for PHP 5.2 and lower
+ */
+define('_JEXEC', 1);
+
+// Constant to identify the CLI installation
+define('_JCLI_INSTALLATION', 1);
+
+// Run the application - All executable code should be triggered through this file
+require_once dirname(__FILE__) . '/includes/cli.php';
diff --git a/installation/language/en-GB/joomla.cli.ini b/installation/language/en-GB/joomla.cli.ini
new file mode 100644
index 00000000000..fe7db95f4d9
--- /dev/null
+++ b/installation/language/en-GB/joomla.cli.ini
@@ -0,0 +1,35 @@
+INSTL_ADMIN_EMAIL_DESC="Enter the email address of the website Super User"
+INSTL_ADMIN_EMAIL_DESC_SHORT="Email address of the website's Super User account"
+INSTL_ADMIN_PASSWORD_DESC="Set the password for your Super User account"
+INSTL_ADMIN_PASSWORD_DESC_SHORT="Password of your Super User account"
+INSTL_ADMIN_USERNAME_DESC="Set the username for your Super User account"
+INSTL_ADMIN_USERNAME_DESC_SHORT="Username of your Super User account"
+INSTL_ADMIN_USER_DESC="Enter the real name of your Super User"
+INSTL_ADMIN_USER_DESC_SHORT="Real name of the Super User account"
+INSTL_DATABASE_COULD_NOT_CONNECT="%s"
+INSTL_DATABASE_ENCRYPTION_CA_LABEL="Path to CA file to verify encryption against"
+INSTL_DATABASE_ENCRYPTION_CA_LABEL_SHORT="Path to CA file to verify encryption against"
+INSTL_DATABASE_ENCRYPTION_CERT_LABEL="Path to the SSL certificate for the database connection. Requires encryption to be set to 2"
+INSTL_DATABASE_ENCRYPTION_CERT_LABEL_SHORT="Path to the SSL certificate for the database connection. Requires encryption to be set to 2"
+INSTL_DATABASE_ENCRYPTION_CIPHER_LABEL="Supported Cipher Suite (optional)"
+INSTL_DATABASE_ENCRYPTION_CIPHER_LABEL_SHORT="Supported Cipher Suite (optional)"
+INSTL_DATABASE_ENCRYPTION_KEY_LABEL="SSL key for the database connection. Requires encryption to be set to 2"
+INSTL_DATABASE_ENCRYPTION_KEY_LABEL_SHORT="SSL key for the database connection. Requires encryption to be set to 2"
+INSTL_DATABASE_ENCRYPTION_MODE_LABEL="Encryption for the database connection. Values: 0=None, 1=One way, 2=Two way"
+INSTL_DATABASE_ENCRYPTION_MODE_LABEL="Encryption for the database connection. Values: 0=None, 1=One way, 2=Two way"
+INSTL_DATABASE_ENCRYPTION_VERIFY_SERVER_CERT_LABEL="Verify SSL certificate for database connection. Values: 0=No, 1=Yes. Requires encryption to be set to 1 or 2"
+INSTL_DATABASE_ENCRYPTION_VERIFY_SERVER_CERT_LABEL_SHORT="Verify SSL certificate for database connection. Values: 0=No, 1=Yes. Requires encryption to be set to 1 or 2"
+INSTL_DATABASE_HOST_DESC="Database host"
+INSTL_DATABASE_HOST_DESC_SHORT="Database host"
+INSTL_DATABASE_NAME_DESC="Database name"
+INSTL_DATABASE_NAME_DESC_SHORT="Database name"
+INSTL_DATABASE_PASSWORD_DESC="Database password"
+INSTL_DATABASE_PASSWORD_DESC_SHORT="Database password"
+INSTL_DATABASE_PREFIX_DESC="Prefix for the database tables"
+INSTL_DATABASE_PREFIX_DESC_SHORT="Prefix for the database tables"
+INSTL_DATABASE_TYPE_DESC="Database type. Supported: mysql, mysqli, pgsql"
+INSTL_DATABASE_TYPE_DESC_SHORT="Database type. Supported by Joomla: mysql (=MySQL (PDO)), mysqli (=MySQLi), pgsql (=PostgreSQL (PDO))"
+INSTL_DATABASE_USER_DESC="Database username"
+INSTL_DATABASE_USER_DESC_SHORT="Database username"
+INSTL_SITE_NAME_DESC="Enter the name of your Joomla site"
+INSTL_SITE_NAME_DESC_SHORT="Name of the website"
diff --git a/installation/src/Application/CliInstallationApplication.php b/installation/src/Application/CliInstallationApplication.php
new file mode 100644
index 00000000000..872ed10fb8d
--- /dev/null
+++ b/installation/src/Application/CliInstallationApplication.php
@@ -0,0 +1,322 @@
+
+ * @license GNU General Public License version 2 or later; see LICENSE.txt
+ */
+
+namespace Joomla\CMS\Installation\Application;
+
+use Joomla\Application\Web\WebClient;
+use Joomla\CMS\Application\CMSApplicationInterface;
+use Joomla\CMS\Application\EventAware;
+use Joomla\CMS\Application\ExtensionNamespaceMapper;
+use Joomla\CMS\Application\IdentityAware;
+use Joomla\CMS\Extension\ExtensionManagerTrait;
+use Joomla\CMS\Factory;
+use Joomla\CMS\Installation\Console\InstallCommand;
+use Joomla\CMS\Language\Language;
+use Joomla\CMS\Language\LanguageHelper;
+use Joomla\CMS\Language\Text;
+use Joomla\CMS\MVC\Factory\MVCFactory;
+use Joomla\CMS\Version;
+use Joomla\Console\Application;
+use Joomla\Database\DatabaseInterface;
+use Joomla\DI\Container;
+use Joomla\DI\ContainerAwareTrait;
+use Joomla\Filesystem\Folder;
+use Joomla\Input\Input;
+use Joomla\Registry\Registry;
+use Joomla\Session\SessionInterface;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * Joomla! Installation Application class.
+ *
+ * @since __DEPLOY_VERSION__
+ */
+final class CliInstallationApplication extends Application implements CMSApplicationInterface
+{
+ use ExtensionNamespaceMapper;
+ use IdentityAware;
+ use ContainerAwareTrait;
+ use EventAware;
+ use ExtensionManagerTrait;
+
+ /**
+ * The application input object.
+ *
+ * @var Input
+ * @since __DEPLOY_VERSION__
+ */
+ public $input;
+
+ /**
+ * The application language object.
+ *
+ * @var Language
+ * @since __DEPLOY_VERSION__
+ */
+ protected $language;
+
+ /**
+ * @var MVCFactory
+ * @since __DEPLOY_VERSION__
+ */
+ protected $mvcFactory;
+
+ /**
+ * Object to imitate the session object
+ *
+ * @var Registry
+ * @since __DEPLOY_VERSION__
+ */
+ protected $session;
+
+ /**
+ * Class constructor.
+ *
+ * @param Input|null $input An optional argument to provide dependency injection for the application's input
+ * object. If the argument is a JInput object that object will become the
+ * application's input object, otherwise a default input object is created.
+ * @param Registry|null $config An optional argument to provide dependency injection for the application's
+ * config object. If the argument is a Registry object that object will become
+ * the application's config object, otherwise a default config object is created.
+ * @param WebClient|null $client An optional argument to provide dependency injection for the application's
+ * client object. If the argument is a WebClient object that object will become the
+ * application's client object, otherwise a default client object is created.
+ * @param Container|null $container Dependency injection container.
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function __construct(
+ ?InputInterface $input = null,
+ ?OutputInterface $output = null,
+ ?Registry $config = null,
+ ?Language $language = null
+ ) {
+ // Register the application name.
+ $this->setName('Joomla CLI installation');
+ $version = new Version();
+ $this->setVersion($version->getShortVersion());
+
+ // Register the client ID.
+ $this->clientId = 2;
+ $this->language = $language;
+
+ // Run the parent constructor.
+ parent::__construct($input, $output, $config);
+
+ // Store the debug value to config based on the JDEBUG flag.
+ $this->config->set('debug', JDEBUG);
+
+ \define('JPATH_COMPONENT', JPATH_BASE);
+ \define('JPATH_COMPONENT_SITE', JPATH_SITE);
+ \define('JPATH_COMPONENT_ADMINISTRATOR', JPATH_ADMINISTRATOR);
+
+ // Register the config to Factory.
+ Factory::$config = $this->config;
+ Factory::$language = $language;
+ }
+
+ /**
+ * Enqueue a system message.
+ *
+ * @param string $msg The message to enqueue.
+ * @param string $type The message type. Default is message.
+ *
+ * @return void
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function enqueueMessage($msg, $type = 'info')
+ {
+ throw new \Exception($msg);
+ }
+
+ /**
+ * Retrieve the application configuration object.
+ *
+ * @return Registry
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function getConfig()
+ {
+ return new Registry();
+ }
+
+ /**
+ * Get the commands which should be registered by default to the application.
+ *
+ * @return \Joomla\Console\Command\AbstractCommand[]
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ protected function getDefaultCommands(): array
+ {
+ return array_merge(
+ parent::getDefaultCommands(),
+ [
+ new InstallCommand(),
+ ]
+ );
+ }
+
+ /**
+ * Method to get the application input object.
+ *
+ * @return \Joomla\Input\Input
+ *
+ * @since 4.0.0
+ */
+ public function getInput(): Input
+ {
+ return new Input();
+ }
+
+ /**
+ * Method to get the application language object.
+ *
+ * @return Language The language object
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function getLanguage()
+ {
+ return $this->language;
+ }
+
+ /**
+ * This is a dummy method, forcing to en-GB on CLI installation
+ *
+ * @return boolean False on failure, array on success.
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function getLocalise()
+ {
+ return false;
+ }
+
+ /**
+ * Returns the installed language files in the administrative and frontend area.
+ *
+ * @param DatabaseInterface|null $db Database driver.
+ *
+ * @return array Array with installed language packs in admin and site area.
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function getLocaliseAdmin(DatabaseInterface $db = null)
+ {
+ $langfiles = array();
+
+ // If db connection, fetch them from the database.
+ if ($db) {
+ foreach (LanguageHelper::getInstalledLanguages() as $clientId => $language) {
+ $clientName = $clientId === 0 ? 'site' : 'admin';
+
+ foreach ($language as $languageCode => $lang) {
+ $langfiles[$clientName][] = $lang->element;
+ }
+ }
+ } else {
+ // Read the folder names in the site and admin area.
+ $langfiles['site'] = Folder::folders(LanguageHelper::getLanguagePath(JPATH_SITE));
+ $langfiles['admin'] = Folder::folders(LanguageHelper::getLanguagePath(JPATH_ADMINISTRATOR));
+ }
+
+ return $langfiles;
+ }
+
+ /**
+ * Get the system message queue. This is a mock
+ * to fullfill the interface requirements and is not functional.
+ *
+ * @return array The system message queue.
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function getMessageQueue()
+ {
+ return [];
+ }
+
+ /**
+ * Get the MVC factory for the installation application
+ *
+ * @return MVCFactory MVC Factory of the installation application
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function getMVCFactory()
+ {
+ if (!$this->mvcFactory) {
+ $this->mvcFactory = new MVCFactory('Joomla\\CMS', $this);
+ }
+
+ return $this->mvcFactory;
+ }
+
+ /**
+ * We need to imitate the session object
+ *
+ * @return SessionInterface Object imitating the session object
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function getSession()
+ {
+ return $this->session;
+ }
+
+ /**
+ * Sets the session for the application to use, if required.
+ *
+ * @param SessionInterface $session A session object.
+ *
+ * @return $this
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function setSession(SessionInterface $session): self
+ {
+ $this->session = $session;
+
+ return $this;
+ }
+
+ /**
+ * Check the client interface by name.
+ *
+ * @param string $identifier String identifier for the application interface
+ *
+ * @return boolean True if this application is of the given type client interface.
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function isClient($identifier)
+ {
+ return 'cli_installation' === $identifier;
+ }
+
+ /**
+ * Flag if the application instance is a CLI or web based application.
+ *
+ * Helper function, you should use the native PHP functions to detect if it is a CLI application.
+ *
+ * @return boolean
+ *
+ * @since __DEPLOY_VERSION__
+ * @deprecated 5.0 Will be removed without replacements
+ */
+ public function isCli()
+ {
+ return $this->isClient('cli_installation');
+ }
+}
diff --git a/installation/src/Console/InstallCommand.php b/installation/src/Console/InstallCommand.php
new file mode 100644
index 00000000000..cf3097d07b2
--- /dev/null
+++ b/installation/src/Console/InstallCommand.php
@@ -0,0 +1,390 @@
+
+ * @license GNU General Public License version 2 or later; see LICENSE.txt
+ */
+
+namespace Joomla\CMS\Installation\Console;
+
+use Joomla\CMS\Factory;
+use Joomla\CMS\Form\FormField;
+use Joomla\CMS\Form\FormHelper;
+use Joomla\CMS\Installation\Model\ChecksModel;
+use Joomla\CMS\Installation\Model\CleanupModel;
+use Joomla\CMS\Installation\Model\DatabaseModel;
+use Joomla\CMS\Installation\Model\SetupModel;
+use Joomla\CMS\Installation\Application\CliInstallationApplication;
+use Joomla\CMS\Language\Text;
+use Joomla\CMS\Version;
+use Joomla\Console\Command\AbstractCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Style\SymfonyStyle;
+
+/**
+ * Console command for installing Joomla
+ *
+ * @since __DEPLOY_VERSION__
+ */
+class InstallCommand extends AbstractCommand
+{
+ /**
+ * The default command name
+ *
+ * @var string
+ * @since __DEPLOY_VERSION__
+ */
+ protected static $defaultName = 'install';
+
+ /**
+ * @var SymfonyStyle
+ * @since __DEPLOY_VERSION__
+ */
+ protected $ioStyle;
+
+ /**
+ * @var InputInterface
+ * @since __DEPLOY_VERSION__
+ */
+ protected $cliInput;
+
+ /**
+ * 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->cliInput = $input;
+ $this->ioStyle = new SymfonyStyle($input, $output);
+
+ $this->ioStyle->title('Install Joomla');
+
+ if (file_exists(JPATH_ROOT . '/configuration.php')) {
+ $this->ioStyle->error('configuration.php already present! Nothing to install, exiting.');
+ return Command::FAILURE;
+ }
+
+ /* @var CliInstallationApplication $app */
+ $app = $this->getApplication();
+
+ /** @var ChecksModel $checkModel */
+ $checkModel = $app->getMVCFactory()->createModel('Checks', 'Installation');
+ $this->ioStyle->write('Checking system requirements...');
+
+ if (!$checkModel->getPhpOptionsSufficient()) {
+ $options = $checkModel->getPhpOptions();
+
+ foreach ($options as $option) {
+ if (!$option->state) {
+ $this->ioStyle->error($option->notice);
+
+ return Command::FAILURE;
+ }
+ }
+ }
+
+ $this->ioStyle->writeln('OK');
+
+ // Collect the configuration
+ $this->ioStyle->write('Collecting configuration...');
+ $cfg = $this->getCLIOptions();
+ $cfg['db_pass_plain'] = $cfg['db_pass'];
+ $cfg['admin_password_plain'] = $cfg['admin_password'];
+ $cfg['language'] = 'en-GB';
+ $cfg['helpurl'] = 'https://help.joomla.org/proxy?keyref=Help{major}{minor}:{keyref}&lang={langcode}';
+ $this->ioStyle->writeln('OK');
+
+ /** @var SetupModel $setupModel */
+ $setupModel = $app->getMVCFactory()->createModel('Setup', 'Installation');
+
+ // Validate DB connection
+ $this->ioStyle->write('Validating DB connection...');
+
+ try {
+ $setupModel->storeOptions($cfg);
+ $setupModel->validateDbConnection();
+ } catch (\Exception $e) {
+ $this->ioStyle->error($e->getMessage());
+
+ return Command::FAILURE;
+ }
+ $this->ioStyle->writeln('OK');
+
+ /** @var DatabaseModel $databaseModel */
+ $databaseModel = $app->getMVCFactory()->createModel('Database', 'Installation');
+
+ // Create and populate database
+ $this->ioStyle->write('Creating and populating the database...');
+ $databaseModel->createDatabase();
+ $db = $databaseModel->initialise();
+
+ // Set the character set to UTF-8 for pre-existing databases.
+ try {
+ $db->alterDbCharacterSet($cfg['db_name']);
+ } catch (\RuntimeException $e) {
+ // Continue Anyhow
+ }
+
+ // Backup any old database.
+ if (!$databaseModel->backupDatabase($db, $cfg['db_prefix'])) {
+ return Command::FAILURE;
+ }
+
+ $files = [
+ 'populate1' => 'base',
+ 'populate2' => 'supports',
+ 'populate3' => 'extensions',
+ 'custom1' => 'localise',
+ 'custom2' => 'custom',
+ ];
+
+ foreach ($files as $step => $schema) {
+ $serverType = $db->getServerType();
+
+ if (\in_array($step, ['custom1', 'custom2']) && !is_file('sql/' . $serverType . '/' . $schema . '.sql')) {
+ continue;
+ }
+
+ $databaseModel->createTables($schema);
+ }
+
+ $this->ioStyle->writeln('OK');
+
+ /** @var \Joomla\CMS\Installation\Model\ConfigurationModel $configurationModel */
+ $configurationModel = $app->getMVCFactory()->createModel('Configuration', 'Installation');
+
+ // Attempt to setup the configuration.
+ $this->ioStyle->write('Writing configuration.php and additional setup ...');
+ $configurationModel->setup($cfg);
+ $this->ioStyle->writeln('OK');
+
+ if (!(new Version())->isInDevelopmentState()) {
+ $this->ioStyle->write('Deleting /installation folder...');
+
+ /** @var CleanupModel $cleanupModel */
+ $cleanupModel = $app->getMVCFactory()->createModel('Cleanup', 'Installation');
+
+ if (!$cleanupModel->deleteInstallationFolder()) {
+ return Command::FAILURE;
+ }
+
+ $this->ioStyle->writeln('OK');
+ }
+
+ $this->ioStyle->success('Joomla has been installed');
+
+ return Command::SUCCESS;
+ }
+
+ /**
+ * Retrieve all necessary options either from CLI options
+ * or from interactive mode.
+ *
+ * @return array Array of configuration options
+ *
+ * @throws \Exception
+ * @since __DEPLOY_VERSION__
+ */
+ protected function getCLIOptions()
+ {
+ /* @var CliInstallationApplication $app */
+ $app = $this->getApplication();
+
+ /* @var SetupModel $setupmodel */
+ $setupmodel = $app->getMVCFactory()->createModel('Setup', 'Installation');
+ $form = $setupmodel->getForm('setup');
+ $cfg = [];
+
+ foreach ($form->getFieldset() as $field) {
+ if (\in_array($field->fieldname, ['language', 'db_old'])) {
+ continue;
+ }
+
+ if ($field->showon) {
+ $conditions = FormHelper::parseShowOnConditions($field->showon, $field->formControl, $field->group);
+ $show = false;
+
+ foreach ($conditions as $cond) {
+ // remove jform[] from the name
+ $f = rtrim(substr($cond['field'], 6), ']');
+ $temp = false;
+
+ if ($cond['sign'] == '=' && \in_array($cfg[$f], $cond['values'])) {
+ $temp = true;
+ } elseif ($cond['sign'] == '!=' && !\in_array($cfg[$f], $cond['values'])) {
+ $temp = true;
+ }
+
+ if ($cond['op'] == '' || $cond['op'] == 'OR') {
+ $show |= $temp;
+ } else {
+ $show &= $temp;
+ }
+ }
+
+ if ($show) {
+ $cfg[$field->fieldname] = $this->getStringFromOption(
+ str_replace('_', '-', $field->fieldname),
+ Text::_((string)$field->getAttribute('label')),
+ $field
+ );
+ } else {
+ $cfg[$field->fieldname] = $field->filter($field->default);
+ }
+ } else {
+ $cfg[$field->fieldname] = $field->filter(
+ $this->getStringFromOption(
+ str_replace('_', '-', $field->fieldname),
+ Text::_((string)$field->getAttribute('label')),
+ $field
+ )
+ );
+ }
+ }
+
+ return $cfg;
+ }
+
+ /**
+ * Configure the command.
+ *
+ * @return void
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ protected function configure(): void
+ {
+ /* @var CliInstallationApplication $app */
+ $app = Factory::getApplication();
+
+ $app->getLanguage()->load('joomla.cli');
+ $help = "%command.name% will install Joomla
+ \nUsage: php %command.full_name%";
+
+ /* @var SetupModel $setupmodel */
+ $setupmodel = $app->getMVCFactory()->createModel('Setup', 'Installation');
+ $form = $setupmodel->getForm('setup');
+
+ $this->setDescription('Install the Joomla CMS');
+
+ foreach ($form->getFieldset() as $field) {
+ if (\in_array($field->fieldname, ['language', 'db_old'])) {
+ continue;
+ }
+
+ $default = $field->getAttribute('default');
+
+ if ($field->fieldname == 'db_prefix') {
+ // Create the random prefix.
+ $prefix = '';
+ $size = 5;
+ $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 .= '_';
+ $default = $prefix;
+ }
+
+ $this->addOption(
+ str_replace('_', '-', $field->fieldname),
+ null,
+ $field->required ? InputOption::VALUE_REQUIRED : InputOption::VALUE_OPTIONAL,
+ Text::_(((string)$field->getAttribute('label')) . '_SHORT'),
+ $default
+ );
+ }
+
+ $this->setHelp($help);
+ }
+
+ /**
+ * Method to get a value from option
+ *
+ * @param string $option set the option name
+ * @param string $question set the question if user enters no value to option
+ * @param FormField $field Field to validate against
+ *
+ * @return string
+ *
+ * @throws \Exception
+ * @since __DEPLOY_VERSION__
+ */
+ protected function getStringFromOption($option, $question, FormField $field): string
+ {
+ // The symfony console unfortunately does not allow to check for parameters given by CLI without the defaults
+ $givenOption = false;
+ $answer = null;
+
+ foreach ($_SERVER['argv'] as $arg) {
+ if ($arg == '--' . $option || strpos($arg, $option . '=')) {
+ $givenOption = true;
+ }
+ }
+
+ // If an option is given via CLI, we validate that value and return it.
+ if ($givenOption || !$this->cliInput->isInteractive()) {
+ $answer = $this->getApplication()->getConsoleInput()->getOption($option);
+
+ if (!is_string($answer)) {
+ throw new \Exception($option . ' has been declared, but has not been given!');
+ }
+
+ $valid = $field->validate($answer);
+
+ if ($valid instanceof \Exception) {
+ throw new \Exception('Value for ' . $option . ' is wrong: ' . $valid->getMessage());
+ }
+
+ return (string) $answer;
+ }
+
+ // We don't have a CLI option and now interactively get that from the user.
+ while (\is_null($answer) || $answer === false) {
+ if (in_array($option, ['admin-password', 'db-pass'])) {
+ $answer = $this->ioStyle->askHidden($question);
+ } else {
+ $answer = $this->ioStyle->ask(
+ $question,
+ $this->getApplication()->getConsoleInput()->getOption($option)
+ );
+ }
+
+ $valid = $field->validate($answer);
+
+ if ($valid instanceof \Exception) {
+ $this->ioStyle->warning('Value for ' . $option . ' is incorrect: ' . $valid->getMessage());
+ $answer = false;
+ }
+
+ if ($option == 'db-pass' && $valid && $answer == null) {
+ return '';
+ }
+ }
+
+ return $answer;
+ }
+}
diff --git a/installation/src/Form/Field/Installation/LanguageField.php b/installation/src/Form/Field/Installation/LanguageField.php
index c2da30066d3..4a22cc2c2e7 100644
--- a/installation/src/Form/Field/Installation/LanguageField.php
+++ b/installation/src/Form/Field/Installation/LanguageField.php
@@ -119,6 +119,12 @@ class LanguageField extends ListField
$app = Factory::getApplication();
+ if ($app->isClient('cli_installation')) {
+ $native = 'en-GB';
+
+ return $native;
+ }
+
// Detect the native language.
$native = LanguageHelper::detectLanguage();
diff --git a/installation/src/Helper/DatabaseHelper.php b/installation/src/Helper/DatabaseHelper.php
index 820750cac4a..3f8920f3e88 100644
--- a/installation/src/Helper/DatabaseHelper.php
+++ b/installation/src/Helper/DatabaseHelper.php
@@ -335,8 +335,9 @@ abstract class DatabaseHelper
*/
public static function checkRemoteDbHost($options)
{
- // Security check for remote db hosts: Check env var if disabled
- $shouldCheckLocalhost = getenv('JOOMLA_INSTALLATION_DISABLE_LOCALHOST_CHECK') !== '1';
+ // Security check for remote db hosts: Check env var if disabled. Also disable in CLI.
+ $shouldCheckLocalhost = getenv('JOOMLA_INSTALLATION_DISABLE_LOCALHOST_CHECK') !== '1'
+ && !defined('_JCLI_INSTALLATION');
// Per default allowed DB hosts: localhost / 127.0.0.1 / ::1 (optionally with port)
$localhost = '/^(((localhost|127\.0\.0\.1|\[\:\:1\])(\:[1-9]{1}[0-9]{0,4})?)|(\:\:1))$/';
diff --git a/installation/src/Model/DatabaseModel.php b/installation/src/Model/DatabaseModel.php
index bd64f317599..287f3bba160 100644
--- a/installation/src/Model/DatabaseModel.php
+++ b/installation/src/Model/DatabaseModel.php
@@ -314,7 +314,7 @@ class DatabaseModel extends BaseInstallationModel
$serverType = $db->getServerType();
// Set the appropriate schema script based on UTF-8 support.
- $schemaFile = 'sql/' . $serverType . '/' . $schema . '.sql';
+ $schemaFile = JPATH_INSTALLATION . '/sql/' . $serverType . '/' . $schema . '.sql';
// Check if the schema is a valid file
if (!is_file($schemaFile)) {
diff --git a/installation/src/Service/Provider/Application.php b/installation/src/Service/Provider/Application.php
index 37bd21f4b9d..ed6473badaf 100644
--- a/installation/src/Service/Provider/Application.php
+++ b/installation/src/Service/Provider/Application.php
@@ -12,9 +12,13 @@ namespace Joomla\CMS\Installation\Service\Provider;
use Joomla\CMS\Error\Renderer\JsonRenderer;
use Joomla\CMS\Factory;
+use Joomla\CMS\Installation\Application\CliInstallationApplication;
use Joomla\CMS\Installation\Application\InstallationApplication;
+use Joomla\CMS\Language\Language;
+use Joomla\CMS\Language\LanguageFactoryInterface;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
+use Joomla\Session\SessionInterface;
use Psr\Log\LoggerInterface;
// phpcs:disable PSR1.Files.SideEffects
@@ -58,6 +62,27 @@ class Application implements ServiceProviderInterface
true
);
+ $container->share(
+ CliInstallationApplication::class,
+ function (Container $container) {
+ $lang = $container->get(LanguageFactoryInterface::class)->createLanguage('en-GB', false);
+
+ $app = new CliInstallationApplication(null, null, $container->get('config'), $lang);
+
+ // The session service provider needs Factory::$application, set it if still null
+ if (Factory::$application === null) {
+ Factory::$application = $app;
+ }
+
+ $app->setDispatcher($container->get('Joomla\Event\DispatcherInterface'));
+ $app->setLogger($container->get(LoggerInterface::class));
+ $app->setSession($container->get(SessionInterface::class));
+
+ return $app;
+ },
+ true
+ );
+
// Inject a custom JSON error renderer
$container->share(
JsonRenderer::class,
diff --git a/libraries/src/Form/Field/PasswordField.php b/libraries/src/Form/Field/PasswordField.php
index 15dea10732b..90184aa2a20 100644
--- a/libraries/src/Form/Field/PasswordField.php
+++ b/libraries/src/Form/Field/PasswordField.php
@@ -175,7 +175,7 @@ class PasswordField extends FormField
$this->minUppercase = 0;
$this->minLowercase = 0;
- if (Factory::getApplication()->get('db') != '') {
+ if (Factory::getApplication()->get('db') != '' && !Factory::getApplication()->isClient('cli_installation')) {
$this->minLength = (int) ComponentHelper::getParams('com_users')->get('minimum_length', 12);
$this->minIntegers = (int) ComponentHelper::getParams('com_users')->get('minimum_integers', 0);
$this->minSymbols = (int) ComponentHelper::getParams('com_users')->get('minimum_symbols', 0);
diff --git a/libraries/src/Form/Rule/PasswordRule.php b/libraries/src/Form/Rule/PasswordRule.php
index 3e17996d24a..f744cdbfa21 100644
--- a/libraries/src/Form/Rule/PasswordRule.php
+++ b/libraries/src/Form/Rule/PasswordRule.php
@@ -58,7 +58,10 @@ class PasswordRule extends FormRule
// In the installer we don't have any access to the
// database yet so use the hard coded default settings
- if (!Factory::getApplication()->isClient('installation')) {
+ if (
+ !Factory::getApplication()->isClient('installation')
+ && !Factory::getApplication()->isClient('cli_installation')
+ ) {
// If we have parameters from com_users, use those instead.
// Some of these may be empty for legacy reasons.
$params = ComponentHelper::getParams('com_users');
diff --git a/ruleset.xml b/ruleset.xml
index ee01c19c05d..aab60ebefbb 100644
--- a/ruleset.xml
+++ b/ruleset.xml
@@ -272,8 +272,10 @@
includes/defines\.php
includes/framework\.php
installation/includes/app\.php
+ installation/includes/cli\.php
installation/includes/defines\.php
installation/index\.php
+ installation/joomla\.php
libraries/cms\.php
libraries/bootstrap\.php
libraries/import\.php
diff --git a/tests/Codeception/drone-api-run.sh b/tests/Codeception/drone-api-run.sh
index a16d2489d91..d8862c8006a 100644
--- a/tests/Codeception/drone-api-run.sh
+++ b/tests/Codeception/drone-api-run.sh
@@ -1,7 +1,11 @@
#!/usr/bin/env bash
set -e
JOOMLA_BASE=$1
-DB_ENGINE=$2
+TEST_SUITE=$2
+DB_ENGINE=$3
+DB_HOST=$4
+DB_PREFIX=$5
+
echo "[RUNNER] Prepare test environment"
@@ -9,15 +13,15 @@ echo "[RUNNER] Prepare test environment"
cd $JOOMLA_BASE
echo "[RUNNER] Copy files to test installation"
-rsync -a --exclude-from=tests/Codeception/exclude.txt $JOOMLA_BASE/ /tests/www/$DB_ENGINE/
-chown -R www-data /tests/www/$DB_ENGINE/
+rsync -a --exclude-from=tests/Codeception/exclude.txt $JOOMLA_BASE/ /tests/www/$TEST_SUITE/
+chown -R www-data /tests/www/$TEST_SUITE/
echo "[RUNNER] Start Apache & Chrome"
apache2ctl -D FOREGROUND &
google-chrome --version
echo "[RUNNER] Start Selenium"
-selenium-standalone start > selenium.api.$DB_ENGINE.log 2>&1 &
+selenium-standalone start > selenium.api.$TEST_SUITE.log 2>&1 &
echo -n "Waiting until Selenium is ready"
until $(curl --output /dev/null --silent --head --fail http://localhost:4444/wd/hub/status); do
printf '.'
@@ -25,13 +29,14 @@ until $(curl --output /dev/null --silent --head --fail http://localhost:4444/wd/
done
echo .
-echo "[RUNNER] Run Codeception"
-cd /tests/www/$DB_ENGINE
-php libraries/vendor/bin/codecept run --fail-fast --steps --debug --env $DB_ENGINE tests/Codeception/acceptance/01-install/
+echo "[RUNNER] Install Joomla"
+cd /tests/www/$TEST_SUITE
+php installation/joomla.php install --verbose --site-name="Joomla CMS test" --admin-email=admin@example.org --admin-username=ci-admin --admin-user="jane doe" --admin-password=joomla-17082005 --db-type=$DB_ENGINE --db-host=$DB_HOST --db-name=test_joomla --db-pass=joomla_ut --db-user=root --db-encryption=0 --db-prefix=$DB_PREFIX
# If you have found this line failing on OSX you need to brew install gnu-sed like we mentioned in the codeception readme!
# This replaces the site secret in configuration.php so we can guarantee a consistent API token for our super user.
-sed -i "/\$secret/c\ public \$secret = 'tEstValue';" /tests/www/$DB_ENGINE/configuration.php
+sed -i "/\$secret/c\ public \$secret = 'tEstValue';" /tests/www/$TEST_SUITE/configuration.php
+echo "[RUNNER] Run Codeception"
# Executing API tests
-php libraries/vendor/bin/codecept run api --fail-fast --steps --debug --env $DB_ENGINE
+php libraries/vendor/bin/codecept run api --fail-fast --steps --debug --env $TEST_SUITE