diff --git a/administrator/components/com_config/src/Model/ApplicationModel.php b/administrator/components/com_config/src/Model/ApplicationModel.php index 3aaa6ce171569..2029a903c3876 100644 --- a/administrator/components/com_config/src/Model/ApplicationModel.php +++ b/administrator/components/com_config/src/Model/ApplicationModel.php @@ -107,7 +107,7 @@ public function getData() // Merge in the session data. if (!empty($temp)) { - $data = array_merge($data, $temp); + $data = array_merge($temp, $data); } // Correct error_reporting value, since we removed "development", the "maximum" should be set instead diff --git a/installation/src/Model/ConfigurationModel.php b/installation/src/Model/ConfigurationModel.php index bc95874789d15..4bc25f5a5d92d 100644 --- a/installation/src/Model/ConfigurationModel.php +++ b/installation/src/Model/ConfigurationModel.php @@ -425,6 +425,7 @@ public function createConfiguration($options) $registry->set('dbsslcipher', $options->db_sslcipher); // Server settings. + $registry->set('force_ssl', 0); $registry->set('live_site', ''); $registry->set('secret', UserHelper::genRandomPassword(16)); $registry->set('gzip', false); diff --git a/libraries/src/Application/ConsoleApplication.php b/libraries/src/Application/ConsoleApplication.php index a4ea32c06abf6..7008fcf6fa441 100644 --- a/libraries/src/Application/ConsoleApplication.php +++ b/libraries/src/Application/ConsoleApplication.php @@ -15,6 +15,7 @@ use Joomla\CMS\Factory; use Joomla\CMS\Language\Language; use Joomla\CMS\Plugin\PluginHelper; +use Joomla\CMS\Version; use Joomla\Console\Application; use Joomla\DI\Container; use Joomla\DI\ContainerAwareTrait; @@ -387,4 +388,16 @@ public function setSession(SessionInterface $session): self return $this; } + + /** + * Flush the media version to refresh versionable assets + * + * @return void + * + * @since 4.0.0 + */ + public function flushAssets() + { + (new Version)->refreshMediaVersion(); + } } diff --git a/libraries/src/Console/CheckJoomlaUpdatesCommand.php b/libraries/src/Console/CheckJoomlaUpdatesCommand.php new file mode 100644 index 0000000000000..8d457da430b34 --- /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): void + { + 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/ExtensionInstallCommand.php b/libraries/src/Console/ExtensionInstallCommand.php new file mode 100644 index 0000000000000..09f3d680305a4 --- /dev/null +++ b/libraries/src/Console/ExtensionInstallCommand.php @@ -0,0 +1,229 @@ +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 + * + * @since 4.0 + * + * @throws \Exception + */ + public function processPathInstallation($path): bool + { + if (!file_exists($path)) + { + $this->ioStyle->warning('The file path specified does not exist.'); + + return false; + } + + $tmpPath = $this->getApplication()->get('tmp_path'); + $tmpPath = $tmpPath . '/' . basename($path); + $package = InstallerHelper::unpack($path, true); + + if ($package['type'] === false) + { + return false; + } + + $jInstaller = Installer::getInstance(); + $result = $jInstaller->install($package['extractdir']); + InstallerHelper::cleanupInstall($tmpPath, $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): bool + { + $filename = InstallerHelper::downloadPackage($url); + + $tmpPath = $this->getApplication()->get('tmp_path'); + + $path = $tmpPath . '/' . 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; + } + + $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; + } + + $this->ioStyle->success('Extension installed successfully.'); + + return self::INSTALLATION_SUCCESSFUL; + } + + $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..7a283dab0f727 --- /dev/null +++ b/libraries/src/Console/ExtensionRemoveCommand.php @@ -0,0 +1,207 @@ +cliInput = $input; + $this->ioStyle = new SymfonyStyle($input, $output); + $language = Factory::getLanguage(); + $language->load('', JPATH_ADMINISTRATOR, null, false, false) || + $language->load('', JPATH_ADMINISTRATOR, null, true); + $language->load('com_installer', JPATH_ADMINISTRATOR, null, false, false)|| + $language->load('com_installer', JPATH_ADMINISTRATOR, null, true); + } + + /** + * Initialise the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + $this->addArgument( + 'extensionId', + 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); + } + + /** + * 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'); + + $response = $this->ioStyle->ask('Are you sure you want to remove this extension?', 'yes/no'); + + if (strtolower($response) === 'yes') + { + // Get an installer object for the extension type + $installer = Installer::getInstance(); + $row = new \Joomla\CMS\Table\Extension(Factory::getDbo()); + + if ((int) $extensionId === 0 || !$row->load($extensionId)) + { + $this->ioStyle->error("Extension with ID of $extensionId not found."); + + return self::REMOVE_NOT_FOUND; + } + + // Do not allow to uninstall locked extensions. + if ((int) $row->locked === 1) + { + $this->ioStyle->error(Text::_('COM_INSTALLER_UNINSTALL_ERROR_LOCKED_EXTENSION')); + + return self::REMOVE_LOCKED; + } + + if ($row->type) + { + if (!$installer->uninstall($row->type, $extensionId)) + { + $this->ioStyle->error('Extension not removed.'); + + return self::REMOVE_FAILED; + } + + $this->ioStyle->success('Extension removed!'); + + return self::REMOVE_SUCCESSFUL; + } + + return self::REMOVE_INVALID_TYPE; + } + elseif (strtolower($response) === 'no') + { + $this->ioStyle->note('Extension not removed.'); + + return self::REMOVE_ABORT; + } + + $this->ioStyle->warning('Invalid response'); + + return self::REMOVE_INVALID_RESPONSE; + } +} diff --git a/libraries/src/Console/ExtensionsListCommand.php b/libraries/src/Console/ExtensionsListCommand.php new file mode 100644 index 0000000000000..abf5c47691ebc --- /dev/null +++ b/libraries/src/Console/ExtensionsListCommand.php @@ -0,0 +1,264 @@ +db = $db; + parent::__construct(); + } + + /** + * 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): void + { + $this->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): void + { + if (!$extensions) + { + $this->extensions = $this->getAllExtensionsFromDB(); + } + else + { + $this->extensions = $extensions; + } + } + + /** + * Retrieves extension list from DB + * + * @return array + * + * @since 4.0 + */ + private function getAllExtensionsFromDB(): array + { + $db = $this->db; + $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): array + { + $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): array + { + $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..4c0d1d56c5f80 --- /dev/null +++ b/libraries/src/Console/GetConfigurationCommand.php @@ -0,0 +1,372 @@ + 'db', + 'options' => [ + 'dbtype', + 'host', + 'user', + 'password', + 'dbprefix', + 'db', + 'dbencryption', + 'dbsslverifyservercert', + 'dbsslkey', + 'dbsslcert', + 'dbsslca', + 'dbsslcipher' + ] + ]; + + /** + * Constant defining the Session option group + * @var array + * @since 4.0 + */ + public const SESSION_GROUP = [ + 'name' => 'session', + 'options' => [ + 'session_handler', + 'shared_session', + 'session_metadata' + ] + ]; + + /** + * Constant defining the Mail option group + * @var array + * @since 4.0 + */ + public const MAIL_GROUP = [ + 'name' => 'mail', + 'options' => [ + 'mailonline', + 'mailer', + 'mailfrom', + 'fromname', + 'sendmail', + 'smtpauth', + 'smtpuser', + 'smtppass', + 'smtphost', + 'smtpsecure', + 'smtpport' + ] + ]; + + /** + * Return code if configuration is get successfully + * @since 4.0 + */ + public const CONFIG_GET_SUCCESSFUL = 0; + + /** + * Return code if configuration group option is not found + * @since 4.0 + */ + public const CONFIG_GET_GROUP_NOT_FOUND = 1; + + /** + * Return code if configuration option is not found + * @since 4.0 + */ + public const CONFIG_GET_OPTION_NOT_FOUND = 2; + + /** + * Return code if the command has been invoked with wrong options + * @since 4.0 + */ + public const CONFIG_GET_OPTION_FAILED = 3; + + /** + * 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): int + { + $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"); + + return self::CONFIG_GET_GROUP_NOT_FOUND; + } + + return self::CONFIG_GET_SUCCESSFUL; + } + + /** + * 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(Array $configs): array + { + $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): int + { + $configs = $this->getApplication()->getConfig()->toArray(); + + if (!array_key_exists($option, $configs)) + { + $this->ioStyle->error("Can't find option *$option* in configuration list"); + + return self::CONFIG_GET_OPTION_NOT_FOUND; + } + + $value = $this->formatConfigValue($this->getApplication()->get($option)); + + $this->ioStyle->table(['Option', 'Value'], [[$option, $value]]); + + return self::CONFIG_GET_SUCCESSFUL; + } + + /** + * Formats the Configuration value + * + * @param mixed $value Value to be formatted + * + * @return string + * + * @since version + */ + protected function formatConfigValue($value): string + { + 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%