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