[4.3] Add a CLI installation for Joomla (#38325)

* Initial code for a CLI installation

* First PoC

* Lots of cleanup

* Finalising CLI installer

* Deleting install folder after installation

* Codestyle

* Fixing path of API test install

* Update installation/INSTALL

Co-authored-by: Brian Teeman <brian@teeman.net>

* Codestyle

* Docblocks

* Wording

* Fixing db prefix for API tests

* Fixing Postgres API tests

* Further fixes to API tests

* Some cleanup

* Fixing API tests one last time

* Docblock formatting

* Resorting fields

* Improving progress indicator messages

* Disable remote DB check for CLI installation

* Fixing check

* Introducing _JCLI_INSTALLATION constant for DB check

* Hiding password entries on CLI

* hide admin psw

* show db_prefix

show db_prefix in interacting mode too

* Adding check for configuration.php present

* Allowing for empty db password

* Deleting installation folder only when not in dev mode

* Codestyle

* Converting strings to ini file

* Update installation/language/en-GB/joomla.cli.ini

Co-authored-by: Brian Teeman <brian@teeman.net>

* Update installation/src/Console/InstallCommand.php

Co-authored-by: Brian Teeman <brian@teeman.net>

* Update installation/src/Console/InstallCommand.php

Co-authored-by: Brian Teeman <brian@teeman.net>

* Update installation/src/Console/InstallCommand.php

Co-authored-by: Brian Teeman <brian@teeman.net>

* Update installation/src/Console/InstallCommand.php

Co-authored-by: Brian Teeman <brian@teeman.net>

* Update installation/src/Console/InstallCommand.php

Co-authored-by: Brian Teeman <brian@teeman.net>

* Update installation/language/en-GB/joomla.cli.ini

Co-authored-by: Brian Teeman <brian@teeman.net>

* Update installation/language/en-GB/joomla.cli.ini

Co-authored-by: Brian Teeman <brian@teeman.net>

* Update installation/language/en-GB/joomla.cli.ini

Co-authored-by: Brian Teeman <brian@teeman.net>

* Update installation/language/en-GB/joomla.cli.ini

Co-authored-by: Brian Teeman <brian@teeman.net>

* Update installation/language/en-GB/joomla.cli.ini

Co-authored-by: Brian Teeman <brian@teeman.net>

* Update installation/language/en-GB/joomla.cli.ini

Co-authored-by: Brian Teeman <brian@teeman.net>

* Update installation/INSTALL

Co-authored-by: Brian Teeman <brian@teeman.net>

* Further bugfixes

* Cleaning up available RDBMS options

* Catching error when connecting to database

* Removing false db type

* Fixing case of attribute

* Removing unnecessary methods from CliInstallationApplication

* Fixing string

* Sorting translation strings for CLI and adding shortened error message

* Fixing docblocks and use statement

* Adding Allons suggestions

* Fixing use statements

* Using language object from app

* Remove unnecessary method

* Fixing language object

* Removing . at the end of sentences since they are followed by a : anyway

* Use - instead of _ in the parameters

* Fixing api tests

Co-authored-by: Brian Teeman <brian@teeman.net>
Co-authored-by: Nicola Galgano <optimus4joomla@gmail.com>
Co-authored-by: Allon Moritz <allon.moritz@digital-peak.com>
This commit is contained in:
Hannes Papenberg 2022-10-21 16:26:07 +02:00 committed by GitHub
parent 81368475ec
commit f340734748
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 940 additions and 38 deletions

View File

@ -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
...

View File

@ -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

View File

@ -15,14 +15,6 @@
class="form-control"
required="true"
/>
<field
name="admin_email"
type="email"
label="INSTL_ADMIN_EMAIL_DESC"
class="form-control"
validate="email"
required="true"
/>
<field
name="admin_user"
type="text"
@ -53,12 +45,20 @@
force="on"
filter="raw"
/>
<field
name="admin_email"
type="email"
label="INSTL_ADMIN_EMAIL_DESC"
class="form-control"
validate="email"
required="true"
/>
<field
name="db_type"
type="databaseconnection"
label="INSTL_DATABASE_TYPE_DESC"
class="form-select"
supported="mysql,mysqli,pgsql,postgresql"
supported="mysql,mysqli,pgsql"
required="true"
default="mysqli"
filter="string"

View File

@ -0,0 +1,63 @@
<?php
/**
* @package Joomla.Installation
* @subpackage Application
*
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
* @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();

39
installation/joomla.php Normal file
View File

@ -0,0 +1,39 @@
<?php
/**
* @package Joomla.Installation
*
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
* @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';

View File

@ -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"

View File

@ -0,0 +1,322 @@
<?php
/**
* @package Joomla.Installation
* @subpackage Application
*
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
* @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');
}
}

View File

@ -0,0 +1,390 @@
<?php
/**
* Joomla! Content Management System
*
* @copyright (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
* @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 = "<info>%command.name%</info> will install Joomla
\nUsage: <info>php %command.full_name%</info>";
/* @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;
}
}

View File

@ -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();

View File

@ -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))$/';

View File

@ -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)) {

View File

@ -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,

View File

@ -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);

View File

@ -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');

View File

@ -272,8 +272,10 @@
<exclude-pattern type="relative">includes/defines\.php</exclude-pattern>
<exclude-pattern type="relative">includes/framework\.php</exclude-pattern>
<exclude-pattern type="relative">installation/includes/app\.php</exclude-pattern>
<exclude-pattern type="relative">installation/includes/cli\.php</exclude-pattern>
<exclude-pattern type="relative">installation/includes/defines\.php</exclude-pattern>
<exclude-pattern type="relative">installation/index\.php</exclude-pattern>
<exclude-pattern type="relative">installation/joomla\.php</exclude-pattern>
<exclude-pattern type="relative">libraries/cms\.php</exclude-pattern>
<exclude-pattern type="relative">libraries/bootstrap\.php</exclude-pattern>
<exclude-pattern type="relative">libraries/import\.php</exclude-pattern>

View File

@ -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