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')); - ?> +
-
-
-
-
- $this)); ?> - changeSet)) : ?> -
- - -
- - - - - - - - - - - - - - - - - - changeSet as $i => $item) : ?> - - manifest_cache); ?> + + 'update-structure')); ?> - - - - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - - - - - - - -
- extension_id, false, 'cid', 'cb', $extension->name); ?> - - name; ?> -
- description); ?> -
-
- client_translated; ?> - - type_translated; ?> - - - - - - - version_id; ?> - - version; ?> - - folder_translated; ?> - - extension_id; ?> -
+ + loadTemplate('update'); ?> + - - pagination->getListFooter(); ?> + + loadTemplate('import'); ?> + - - - - -
-
-
+
diff --git a/administrator/components/com_installer/tmpl/database/default_import.php b/administrator/components/com_installer/tmpl/database/default_import.php new file mode 100644 index 0000000000000..e0c73d73b4a47 --- /dev/null +++ b/administrator/components/com_installer/tmpl/database/default_import.php @@ -0,0 +1,73 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\HTML\HTMLHelper; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Utility\Utility; + +?> + +
+ + +
+ +
+ + + + + + + + + + + + + + +
+ + + +
+ +
+ + +
  + +
+
+ + + + diff --git a/administrator/components/com_installer/tmpl/database/default_update.php b/administrator/components/com_installer/tmpl/database/default_update.php new file mode 100644 index 0000000000000..518a5eaf891bb --- /dev/null +++ b/administrator/components/com_installer/tmpl/database/default_update.php @@ -0,0 +1,122 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\HTML\HTMLHelper; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Layout\LayoutHelper; + +$listOrder = $this->escape($this->state->get('list.ordering')); +$listDirection = $this->escape($this->state->get('list.direction')); +?> +
+
+
+ $this)); ?> + changeSet)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + + + changeSet as $i => $item) : ?> + + manifest_cache); ?> + + + + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + + + + + + + +
+ extension_id, false, 'cid', 'cb', $extension->name); ?> + + name; ?> +
+ description); ?> +
+
+ client_translated; ?> + + type_translated; ?> + + + + + + + version_id; ?> + + version; ?> + + folder_translated; ?> + + extension_id; ?> +
+ + + pagination->getListFooter(); ?> + + + + + +
+
+
diff --git a/administrator/language/en-GB/com_installer.ini b/administrator/language/en-GB/com_installer.ini index 6025be0908917..1560c2752c8a5 100644 --- a/administrator/language/en-GB/com_installer.ini +++ b/administrator/language/en-GB/com_installer.ini @@ -48,6 +48,7 @@ COM_INSTALLER_FIELD_EXTRA_QUERY_LABEL="Download Key" COM_INSTALLER_FIELD_NAME_LABEL="Name" COM_INSTALLER_FIELD_TYPE_LABEL="Type" COM_INSTALLER_FIELD_TYPE_LOCATION="Location" +COM_INSTALLER_FILE_IMPORTER_TEXT="Database zip file to import" COM_INSTALLER_FILTER_LABEL="Search by Extension Name" COM_INSTALLER_HEADER_DATABASE="Maintenance: Database" COM_INSTALLER_HEADER_DISCOVER="Extensions: Discover" @@ -86,6 +87,8 @@ COM_INSTALLER_HEADING_UPDATESITE_NAME="Update Site" COM_INSTALLER_HEADING_UPDATESITE_NAME_ASC="Update Site ascending" COM_INSTALLER_HEADING_UPDATESITE_NAME_DESC="Update Site descending" COM_INSTALLER_HEADING_UPDATESITEID="ID" +COM_INSTALLER_IMPORT_BUTTON="Import" +COM_INSTALLER_IMPORT_TITLE="Upload & Import" COM_INSTALLER_INSTALL_ARIA="Install %s" COM_INSTALLER_INSTALL_BUTTON="Install" COM_INSTALLER_INSTALL_CHECKSUM_WARNING="No checksum found on the update server." @@ -126,6 +129,14 @@ COM_INSTALLER_MSG_DATABASE_ERRORS="%d Problems" COM_INSTALLER_MSG_DATABASE_ERRORS_0="No Problems" COM_INSTALLER_MSG_DATABASE_ERRORS_1="One Problem" COM_INSTALLER_MSG_DATABASE_FILTER_ERROR="No default text filters found." +COM_INSTALLER_MSG_DATABASE_IMPORT_CHECK_ERROR="This archive does not conform to the structure of your database: %s." +COM_INSTALLER_MSG_DATABASE_IMPORT_DATA_ERROR="Unable to import data from table %s." +COM_INSTALLER_MSG_DATABASE_IMPORT_DROP_ERROR="Unable to drop table %s." +COM_INSTALLER_MSG_DATABASE_IMPORT_ERROR="Unable to import file %s to the database." +COM_INSTALLER_MSG_DATABASE_IMPORT_EXTRACT_ERROR="Unable to extract file %1$s into %2$s." +COM_INSTALLER_MSG_DATABASE_IMPORT_MERGE_ERROR="Unable to merge structure from table %s." +COM_INSTALLER_MSG_DATABASE_IMPORT_OK="All tables imported." +COM_INSTALLER_MSG_DATABASE_IMPORT_UPLOAD_ERROR="Unable to upload file %s." COM_INSTALLER_MSG_DATABASE_INFO="Other Information" COM_INSTALLER_MSG_DATABASE_RENAME_TABLE="Table %2$s does not exist. (From file %1$s.)" COM_INSTALLER_MSG_DATABASE_SCHEMA_ERROR="Database version (%1$s) does not match manifest version (%2$s)." @@ -226,6 +237,7 @@ COM_INSTALLER_TITLE_LANGUAGES="Extensions: Install Languages" COM_INSTALLER_TITLE_MANAGE="Extension: Manage" COM_INSTALLER_TITLE_UPDATE="Extension: Update" COM_INSTALLER_TITLE_UPDATESITES="Extensions: Update Sites" +COM_INSTALLER_TOOLBAR_DATABASE_EXPORT="Export" COM_INSTALLER_TOOLBAR_DATABASE_FIX="Update Structure" COM_INSTALLER_TOOLBAR_DISCOVER="Discover" COM_INSTALLER_TOOLBAR_FIND_LANGUAGES="Find languages" @@ -291,6 +303,9 @@ COM_INSTALLER_VALUE_SUPPORTED_MISSING="Download Key invalid" COM_INSTALLER_VALUE_SUPPORTED_SELECT="- Select Download Key -" COM_INSTALLER_VALUE_SUPPORTED_SUPPORTED="Download Key supported" COM_INSTALLER_VALUE_TYPE_SELECT="- Select Type -" +COM_INSTALLER_VIEW_DEFAULT_IMPORT_INTRO="You can use this feature to import Joomla database if you know what you are doing. First export the Joomla database in ZIP format from the Export button. Then use the fields below to upload and import it." +COM_INSTALLER_VIEW_DEFAULT_TAB_FIX="Check Structure" +COM_INSTALLER_VIEW_DEFAULT_TAB_IMPORT="Upload & Import" COM_INSTALLER_XML_DESCRIPTION="Installer component for adding, removing and upgrading extensions" JLIB_RULES_SETTING_NOTES_COM_INSTALLER="Changes apply to this component only.
Inherited - a Global Configuration setting or higher level setting is applied.
Denied always wins - whatever is set at the Global or higher level and applies to all child elements.
Allowed will enable the action for this component unless overruled by a Global Configuration setting." ; Alternate language strings for the rules form field