diff --git a/administrator/components/com_installer/src/Controller/DatabaseController.php b/administrator/components/com_installer/src/Controller/DatabaseController.php index 54cb718324967..a1686f3845434 100644 --- a/administrator/components/com_installer/src/Controller/DatabaseController.php +++ b/administrator/components/com_installer/src/Controller/DatabaseController.php @@ -36,6 +36,9 @@ class DatabaseController extends BaseController */ public function fix() { + // Specify the title of the message + $title = sprintf('[%s]', Text::sprintf('COM_INSTALLER_VIEW_DEFAULT_TAB_FIX')); + // Check for request forgeries. $this->checkToken(); @@ -44,6 +47,7 @@ public function fix() if (!is_array($cid) || count($cid) < 1) { + $this->app->getLogger()->warning($title, array('category' => 'jerror')); $this->app->getLogger()->warning( Text::_( 'COM_INSTALLER_ERROR_NO_EXTENSIONS_SELECTED' @@ -68,6 +72,77 @@ public function fix() $this->setRedirect(Route::_('index.php?option=com_installer&view=database', false)); } + /** + * Export all the database via XML + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function export() + { + if ($view = $this->getView('Database', 'raw')) + { + /** @var DatabaseModel $model */ + $model = $this->getModel('Database'); + + if ($model->export()) + { + // Push the model into the view (as default). + $view->setModel($model, true); + + // Push document object into the view. + $view->document = $this->app->getDocument(); + + $view->display(); + } + } + } + + /** + * Import all the database via XML + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function import() + { + // Specify the title of the message + $title = sprintf('[%s]', Text::sprintf('COM_INSTALLER_VIEW_DEFAULT_TAB_IMPORT')); + + // Get file to import in the database. + $file = $this->input->files->get('zip_file', null, 'raw'); + + if ($file['name'] == '') + { + $this->app->getLogger()->warning($title, ['category' => 'jerror']); + $this->app->getLogger()->warning( + Text::_( + 'COM_INSTALLER_MSG_INSTALL_NO_FILE_SELECTED' + ), ['category' => 'jerror'] + ); + } + else + { + /** @var DatabaseModel $model */ + $model = $this->getModel('Database'); + + if ($model->import($file)) + { + $this->app->enqueueMessage($title, 'message'); + $this->setMessage(Text::_('COM_INSTALLER_MSG_DATABASE_IMPORT_OK')); + } + else + { + $this->app->enqueueMessage($title, 'error'); + $this->setMessage(Text::sprintf('COM_INSTALLER_MSG_DATABASE_IMPORT_ERROR', $file['name']), 'error'); + } + } + + $this->setRedirect(Route::_('index.php?option=com_installer&view=database', false)); + } + /** * Provide the data for a badge in a menu item via JSON * diff --git a/administrator/components/com_installer/src/Model/DatabaseModel.php b/administrator/components/com_installer/src/Model/DatabaseModel.php index ed585f253dfd6..b71bc3f9b9c60 100644 --- a/administrator/components/com_installer/src/Model/DatabaseModel.php +++ b/administrator/components/com_installer/src/Model/DatabaseModel.php @@ -11,7 +11,10 @@ \defined('_JEXEC') or die; +use Joomla\Archive\Archive; use Joomla\CMS\Component\ComponentHelper; +use Joomla\CMS\Factory; +use Joomla\CMS\Filesystem\Path; use Joomla\CMS\Language\Text; use Joomla\CMS\MVC\Factory\MVCFactoryInterface; use Joomla\CMS\Schema\ChangeSet; @@ -20,7 +23,11 @@ use Joomla\Component\Installer\Administrator\Helper\InstallerHelper; use Joomla\Database\DatabaseQuery; use Joomla\Database\Exception\ExecutionFailureException; +use Joomla\Database\Exception\UnsupportedAdapterException; use Joomla\Database\ParameterType; +use Joomla\Filesystem\Exception\FilesystemException; +use Joomla\Filesystem\File; +use Joomla\Filesystem\Folder; use Joomla\Registry\Registry; \JLoader::register('JoomlaInstallerScript', JPATH_ADMINISTRATOR . '/components/com_admin/script.php'); @@ -337,6 +344,233 @@ public function fix($cids = array()) } } + /** + * Get the filename of the temporary database archive + * + * @return string + * + * @since __DEPLOY_VERSION__ + */ + public function getZipFilename() + { + return Factory::getApplication()->get('tmp_path') . '/joomla_db.zip'; + } + + /** + * Export all the database via XML + * + * @return boolean + * + * @since __DEPLOY_VERSION__ + */ + public function export() + { + $db = $this->getDbo(); + + // Make sure the database supports exports before we get going + try + { + $exporter = $db->getExporter()->withStructure(); + } + catch (UnsupportedAdapterException $e) + { + return false; + } + + $tables = $db->getTableList(); + $prefix = $db->getPrefix(); + + $zipFile = $this->getZipFilename(); + $zipArchive = (new Archive)->getAdapter('zip'); + + foreach ($tables as $table) + { + if (strpos($table, $prefix) === 0) + { + $data = (string) $exporter->from($table)->withData(true); + $zipFilesArray[] = ['name' => $table . '.xml', 'data' => $data]; + $zipArchive->create($zipFile, $zipFilesArray); + } + } + + return true; + } + + /** + * Checks if the zip file contains database export files + * + * @param string $file A zip archive to analyze + * + * @return void + * + * @since __DEPLOY_VERSION__ + * @throws \RuntimeException + */ + private function checkZipFile($archive) + { + $db = $this->getDbo(); + $zip = zip_open($archive); + + if (!\is_resource($zip)) + { + throw new \RuntimeException('Unable to open archive'); + } + + while ($file = @zip_read($zip)) + { + if (strpos(zip_entry_name($file), $db->getPrefix()) === false) + { + zip_entry_close($file); + @zip_close($zip); + throw new \RuntimeException('Unable to find prefix'); + } + + zip_entry_close($file); + } + + @zip_close($zip); + } + + /** + * Import all the database via XML + * + * @param string $file A zip archive to extract + * + * @return boolean + * + * @since __DEPLOY_VERSION__ + */ + public function import($file) + { + // Specify the title of the message + $title = sprintf('[%s]', Text::sprintf('COM_INSTALLER_VIEW_DEFAULT_TAB_IMPORT')); + + $app = Factory::getApplication(); + $db = $this->getDbo(); + + // Make sure that file uploads are enabled in php. + if (!(bool) ini_get('file_uploads')) + { + $app->enqueueMessage($title, 'error'); + $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLFILE'), 'error'); + + return false; + } + + $tmpFile = $app->get('tmp_path') . '/' . $file['name']; + + try + { + File::upload($file['tmp_name'], $tmpFile); + } + catch (FilesystemException $e) + { + $app->enqueueMessage($title, 'error'); + $app->enqueueMessage(Text::sprintf('COM_INSTALLER_MSG_DATABASE_IMPORT_UPLOAD_ERROR', $file['name']), 'error'); + + return false; + } + + try + { + $this->checkZipFile($tmpFile); + } + catch (\RuntimeException $e) + { + $app->enqueueMessage($title, 'error'); + $app->enqueueMessage(Text::sprintf('COM_INSTALLER_MSG_DATABASE_IMPORT_CHECK_ERROR', $e->getMessage()), 'error'); + unlink($tmpFile); + + return false; + } + + $destDir = Path::clean($app->get('tmp_path') . '/'); + $zipArchive = (new Archive)->getAdapter('zip'); + + try + { + $zipArchive->extract($tmpFile, $destDir); + } + catch (\RuntimeException $e) + { + $app->enqueueMessage($title, 'error'); + $app->enqueueMessage(Text::sprintf('COM_INSTALLER_MSG_DATABASE_IMPORT_EXTRACT_ERROR', $tmpFile, $destDir), 'error'); + unlink($tmpFile); + + return false; + } + + try + { + $importer = $db->getImporter() + ->withStructure() + ->asXml(); + } + catch (UnsupportedAdapterException $e) + { + unlink($tmpFile); + + return false; + } + + $tables = Folder::files($destDir, '\.xml$'); + + foreach ($tables as $table) + { + $tableFile = $destDir . '/' . $table; + $tableName = str_replace('.xml', '', $table); + $importer->from(file_get_contents($tableFile)); + + try + { + $db->dropTable($tableName, true); + } + catch (ExecutionFailureException $e) + { + unlink($tableFile); + unlink($tmpFile); + $app->enqueueMessage($title, 'error'); + $app->enqueueMessage(Text::sprintf('COM_INSTALLER_MSG_DATABASE_IMPORT_DROP_ERROR', $tableName), 'error'); + + return false; + } + + try + { + $importer->mergeStructure(); + } + catch (\Exception $e) + { + unlink($tableFile); + unlink($tmpFile); + $app->enqueueMessage($title, 'error'); + $app->enqueueMessage(Text::sprintf('COM_INSTALLER_MSG_DATABASE_IMPORT_MERGE_ERROR', $tableName), 'error'); + + return false; + } + + try + { + $importer->importData(); + } + catch (\Exception $e) + { + unlink($tableFile); + unlink($tmpFile); + $app->enqueueMessage($title, 'error'); + $app->enqueueMessage(Text::sprintf('COM_INSTALLER_MSG_DATABASE_IMPORT_DATA_ERROR', $tableName), 'error'); + + return false; + } + + unlink($tableFile); + } + + unlink($tmpFile); + + return true; + } + /** * Gets the changeset array. * diff --git a/administrator/components/com_installer/src/View/Database/HtmlView.php b/administrator/components/com_installer/src/View/Database/HtmlView.php index 772e86d40b6d2..d6a9092d878b6 100644 --- a/administrator/components/com_installer/src/View/Database/HtmlView.php +++ b/administrator/components/com_installer/src/View/Database/HtmlView.php @@ -82,6 +82,9 @@ public function display($tpl = null) // Get the application $app = Factory::getApplication(); + // Specify the title of the message + $title = sprintf('[%s]', Text::sprintf('COM_INSTALLER_VIEW_DEFAULT_TAB_FIX')); + // Get data from the model. /** @var DatabaseModel $model */ $model = $this->getModel(); @@ -92,6 +95,7 @@ public function display($tpl = null) } catch (\Exception $exception) { + $app->enqueueMessage($title, 'error'); $app->enqueueMessage($exception->getMessage(), 'error'); } @@ -102,9 +106,16 @@ public function display($tpl = null) if ($this->changeSet) { - ($this->errorCount === 0) - ? $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_DATABASE_CORE_OK'), 'info') - : $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_DATABASE_CORE_ERRORS'), 'warning'); + if ($this->errorCount === 0) + { + $app->enqueueMessage($title); + $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_DATABASE_CORE_OK')); + } + else + { + $app->enqueueMessage($title, 'warning'); + $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_DATABASE_CORE_ERRORS'), 'warning'); + } } parent::display($tpl); @@ -124,6 +135,8 @@ protected function addToolbar() */ ToolbarHelper::custom('database.fix', 'refresh', '', 'COM_INSTALLER_TOOLBAR_DATABASE_FIX', true); ToolbarHelper::divider(); + ToolbarHelper::custom('database.export', 'download', 'download', 'COM_INSTALLER_TOOLBAR_DATABASE_EXPORT', false); + ToolbarHelper::divider(); parent::addToolbar(); ToolbarHelper::help('Information:_Database'); } diff --git a/administrator/components/com_installer/src/View/Database/RawView.php b/administrator/components/com_installer/src/View/Database/RawView.php new file mode 100644 index 0000000000000..6bd6d66527df0 --- /dev/null +++ b/administrator/components/com_installer/src/View/Database/RawView.php @@ -0,0 +1,65 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Installer\Administrator\View\Database; + +defined('_JEXEC') or die; + +use Joomla\CMS\Application\CMSApplication; +use Joomla\CMS\Factory; +use Joomla\CMS\Filter\OutputFilter; +use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; +use Joomla\Component\Installer\Administrator\Model\DatabaseModel; + +/** + * Class view to download the database snapshot. + * + * @since __DEPLOY_VERSION__ + */ +class RawView extends BaseHtmlView +{ + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since __DEPLOY_VERSION__ + * + * @throws Exception + */ + public function display($tpl = null): void + { + /** @var CMSApplication $app */ + $app = Factory::getApplication(); + + /** @var DatabaseModel $model */ + $model = $this->getModel(); + + // Send the exporter archive to the browser as a download + $zipFile = $model->getZipFilename(); + $download = OutputFilter::stringURLSafe($app->get('sitename')) . '_DB_' . date("Y-m-d\TH-i-s") . '.zip'; + + $this->document->setMimeEncoding('application/zip'); + + $app->setHeader( + 'Content-disposition', + 'attachment; filename="' . $download . '"', + true + ) + ->setHeader('Content-Length', filesize($zipFile), true) + ->sendHeaders(); + + ob_end_clean(); + readfile($zipFile); + flush(); + unlink($zipFile); + } +} diff --git a/administrator/components/com_installer/tmpl/database/default.php b/administrator/components/com_installer/tmpl/database/default.php index 9a48c76880266..f83e68cd3f4e1 100644 --- a/administrator/components/com_installer/tmpl/database/default.php +++ b/administrator/components/com_installer/tmpl/database/default.php @@ -11,120 +11,24 @@ use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Language\Text; -use Joomla\CMS\Layout\LayoutHelper; use Joomla\CMS\Router\Route; HTMLHelper::_('behavior.multiselect'); -$listOrder = $this->escape($this->state->get('list.ordering')); -$listDirection = $this->escape($this->state->get('list.direction')); - ?> +
| + + | ++ + | ++ + | ++ + | ++ + | ++ + | ++ + | ++ + | ++ + | +
|---|---|---|---|---|---|---|---|---|
| + extension_id, false, 'cid', 'cb', $extension->name); ?> + | +
+ name; ?>
+
+ description); ?>
+
+ |
+ + client_translated; ?> + | ++ type_translated; ?> + | +
+
+
+
+
+
+
+
|
+ + version_id; ?> + | ++ version; ?> + | ++ folder_translated; ?> + | ++ extension_id; ?> + | +