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