<?php /** * Akeeba Engine * The modular PHP5 site backup engine * * @copyright Copyright (c)2006-2017 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU GPL version 3 or, at your option, any later version * @package akeebaengine * */ namespace Akeeba\Engine\Dump\Reverse; // Protection against direct access defined('AKEEBAENGINE') or die(); use Akeeba\Engine\Driver\Base as DriverBase; use Akeeba\Engine\Dump\Native\Mysql as NativeMysql; use Akeeba\Engine\Factory; use Psr\Log\LogLevel; /** * A MySQL database dump class, using reverse engineering of the * INFORMATION_SCHEMA views to deduce the DDL of the database entities. * * Configuration parameters: * host <string> MySQL database server host name or IP address * port <string> MySQL database server port (optional) * username <string> MySQL user name, for authentication * password <string> MySQL password, for authentication * database <string> MySQL database * dumpFile <string> Absolute path to dump file; must be writable (optional; if left blank it is * automatically calculated) */ class Mysql extends NativeMysql { /** * Implements the constructor of the class * * @return Mysql */ public function __construct() { Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: New instance"); } protected function getTablesToBackup() { $configuration = Factory::getConfiguration(); $notracking = $configuration->get('engine.dump.native.nodependencies', 0); // First, get a map of table names <--> abstract names $this->reverse_engineer_db(); if ($this->getError()) { return; } if (!$notracking) { // Process dependencies and rearrange tables respecting them $this->process_dependencies(); if ($this->getError()) { return; } // Remove dependencies array $this->dependencies = array(); } } protected function reverse_engineer_db() { // Get a database connection Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Reverse engineering database"); if ($this->getError()) { return; } // Reset internal tables $this->table_name_map = array(); $this->tables_data = array(); $this->dependencies = array(); // Clone the db object and point it to information_schema $dbi = clone $this->getDB(); if (!$dbi->select('information_schema')) { Factory::getLog()->log(LogLevel::ERROR, __CLASS__ . " :: Could not connect to the INFORMATION_SCHEMA database"); } // Get the list of all database tables and views Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Reverse engineering Tables"); $this->reverse_engineer_tables($dbi); $this->reverse_engineer_views($dbi); // If we have MySQL > 5.0 add the list of stored procedures, stored functions // and triggers, but only if user has allows that and the target compatibility is // not MySQL 4! Also, if dependency tracking is disabled, we won't dump triggers, // functions and procedures. $registry = Factory::getConfiguration(); $enable_entities = $registry->get('engine.dump.native.advanced_entitites', true); $notracking = $registry->get('engine.dump.native.nodependencies', 0); if ($enable_entities && ($notracking == 0)) { // @todo Triggers // Factory::getLog()->log(LogLevel::DEBUG, __CLASS__." :: Reverse engineering Triggers"); // @todo Functions // Factory::getLog()->log(LogLevel::DEBUG, __CLASS__." :: Reverse engineering Functions"); // @todo Procedures // Factory::getLog()->log(LogLevel::DEBUG, __CLASS__." :: Reverse engineering Procedures"); } $dbi->select($this->database); } /** * Reverse engineers the Table definitions of this database * * @param DriverBase $dbi Database connection to INFORMATION_SCHEMA */ protected function reverse_engineer_tables(&$dbi) { $schema_name = $this->database; $sql = 'SELECT * FROM `tables` WHERE `table_schema` = ' . $dbi->quote($schema_name); $dbi->setQuery($sql); $all_tables = $dbi->loadObjectList(); $registry = Factory::getConfiguration(); $root = $registry->get('volatile.database.root', '[SITEDB]'); // If we have filters, make sure the tables pass the filtering $filters = Factory::getFilters(); if (!empty($all_tables)) { foreach ($all_tables as $table_object) { // Extract the table name $table_name = $table_object->TABLE_NAME; // Skip system objects if ($table_object->TABLE_TYPE != 'BASE TABLE') { continue; } // Filter and convert if (substr($table_name, 0, 3) == '#__') { $this->setWarning(__CLASS__ . " :: Table $table_name has a prefix of #__. This would cause restoration errors; table skipped."); continue; } $table_abstract = $this->getAbstract($table_name); if (substr($table_abstract, 0, 4) != 'bak_') // Skip backup tables { // Apply exclusion filters if (!$filters->isFiltered($table_abstract, $root, 'dbobject', 'all')) { Factory::getLog()->log(LogLevel::INFO, __CLASS__ . " :: Adding $table_name (internal name $table_abstract)"); $this->table_name_map[$table_name] = $table_abstract; } else { Factory::getLog()->log(LogLevel::INFO, __CLASS__ . " :: Skipping $table_name (internal name $table_abstract)"); continue; } } else { Factory::getLog()->log(LogLevel::INFO, __CLASS__ . " :: Backup table $table_name automatically skipped."); continue; } // Still here? The table is added. We now have to store its // create command, dependency info and so on $new_entry = array( 'type' => 'table', 'dump_records' => true ); switch ($table_object->ENGINE) { // Merge tables case 'MRG_MYISAM': $new_entry['type'] = 'merge'; $new_entry['dump_records'] = false; break; // Tables whose data we do not back up (memory, federated and can-have-no-data tables) case 'MEMORY': case 'EXAMPLE': case 'BLACKHOLE': case 'FEDERATED': $new_entry['dump_records'] = false; break; // Normal tables default: break; } // Table Data Filter - skip dumping table contents of filtered out tables if ($filters->isFiltered($table_abstract, $root, 'dbobject', 'content')) { $new_entry['dump_records'] = false; } $new_entry['create'] = $this->get_create_table($dbi, $table_name, $table_abstract, $table_object, $dependencies); $new_entry['dependencies'] = $dependencies; $this->tables_data[$table_name] = $new_entry; } } } /** * Gets the CREATE TABLE command of a given table * * @param DriverBase $dbi The db connection to the INFORMATION_SCHEMA db * @param string $table_name The name of the table * @param string $table_abstract The abstract name of the table * @param \stdClass $table_object The TABLES object for this table * @param array $dependencies Dependency tracking information * * @return string The CREATE TABLE definition */ protected function get_create_table(&$dbi, $table_name, $table_abstract, $table_object, &$dependencies) { $configuration = Factory::getConfiguration(); $notracking = $configuration->get('engine.dump.native.nodependencies', 0); $useabstract = Factory::getEngineParamsProvider()->getScriptingParameter('db.abstractnames', 1); $columns_sql = array(); $keys_sql = array(); $constraints_sql = array(); // ===================================================================== // ========== GENERATE SQL FOR COLUMNS // ===================================================================== // Get columns $query = 'SELECT * FROM `columns` WHERE `table_schema` = ' . $dbi->quote($this->database) . ' AND `table_name` = ' . $dbi->quote($table_name) . ' ORDER BY `ordinal_position` ASC'; $dbi->setQuery($query); $allColumns = $dbi->loadObjectList(); foreach ($allColumns as $oColumn) { $line = $dbi->quoteName($oColumn->COLUMN_NAME) . ' ' . $oColumn->COLUMN_TYPE . ' '; $line .= ($oColumn->IS_NULLABLE == 'YES') ? 'NULL ' : 'NOT NULL '; if (!in_array($oColumn->EXTRA, array('CURRENT_TIMESTAMP'))) { $line .= ($oColumn->EXTRA == '') ? '' : ($oColumn->EXTRA . ' '); } if (in_array($oColumn->COLUMN_DEFAULT, array('CURRENT_TIMESTAMP'))) { $line .= ' DEFAULT ' . $oColumn->COLUMN_DEFAULT; } else { $line .= ($oColumn->COLUMN_DEFAULT == '') ? '' : (' DEFAULT ' . $dbi->quote($oColumn->COLUMN_DEFAULT) . ' '); } $line .= ($oColumn->COLUMN_COMMENT == '') ? '' : (' COMMENT ' . $dbi->quote($oColumn->COLUMN_COMMENT)); $columns_sql[] = $line; } // ===================================================================== // ========== GENERATE SQL FOR KEYS AND INDICES // ===================================================================== // Get the primary and unique key names $query = 'SELECT `constraint_name`, `constraint_type` FROM `table_constraints` WHERE `table_schema` = ' . $dbi->quote($this->database) . ' AND `table_name` = ' . $dbi->quote($table_name) . ' AND `constraint_type` IN(' . $dbi->quote('PRIMARY KEY') . ', ' . $dbi->quote('UNIQUE') . ')'; $dbi->setQuery($query); $specialKeys = $dbi->loadObjectList('constraint_name'); // Get the columns per key and key information $query = 'SELECT * FROM `statistics` WHERE `table_schema` = ' . $dbi->quote($this->database) . ' AND `table_name` = ' . $dbi->quote($table_name) . ' ORDER BY `INDEX_NAME` ASC, SEQ_IN_INDEX ASC'; $dbi->setQuery($query); $allColumns = $dbi->loadObjectList(); $rawKeys = array(); if (!empty($allColumns)) { foreach ($allColumns as $oColumn) { if (!array_key_exists($oColumn->INDEX_NAME, $rawKeys)) { $entry = array( 'name' => $oColumn->INDEX_NAME, 'def' => 'KEY', 'columns' => array(), 'type' => 'BTREE', ); if (array_key_exists($oColumn->INDEX_NAME, $specialKeys)) { $entry['def'] = $specialKeys[$oColumn->INDEX_NAME]->constraint_type; if ($entry['def'] == 'UNIQUE') { $entry['def'] = 'UNIQUE KEY'; } elseif ($entry['def'] == 'PRIMARY') { $entry['def'] = 'PRIMARY KEY'; } } $rawKeys[$oColumn->INDEX_NAME] = $entry; } // This is the optional key length for each column $subpart = ''; if ($oColumn->SUB_PART) { $subpart = '(' . $oColumn->SUB_PART . ')'; } // Add the column to the index $rawKeys[$oColumn->INDEX_NAME]['columns'][] = '`' . $oColumn->COLUMN_NAME . '`' . $subpart; // Setup the index type $rawKeys[$oColumn->INDEX_NAME]['type'] = $oColumn->INDEX_TYPE; } } // Piece together the keys' SQL statements if (!empty($rawKeys)) { foreach ($rawKeys as $keydef) { $line = ' ' . $keydef['def'] . ' '; if ($keydef['type'] == 'FULLTEXT') { $line = ' ' . $keydef['type'] . $line; $keydef['type'] = ''; } if ($keydef['def'] != 'PRIMARY KEY') { $line .= "`{$keydef['name']}` "; } $line .= '('; $line .= implode(',', $keydef['columns']); $line .= ')'; if (!empty($keydef['type']) && ($keydef['def'] != 'PRIMARY KEY')) { $line .= ' USING ' . $keydef['type']; } $keys_sql[] = $line; } } // ===================================================================== // ========== GENERATE SQL FOR CONSTRAINTS // ===================================================================== // Get the foreign key names $query = 'SELECT * FROM `referential_constraints` WHERE `constraint_schema` = ' . $dbi->quote($this->database) . ' AND `table_name` = ' . $dbi->quote($table_name); $dbi->setQuery($query); $foreignKeyInfo = $dbi->loadObjectList('CONSTRAINT_NAME'); // Get the columns per key and key information $query = 'SELECT * FROM `key_column_usage` WHERE `constraint_schema` = ' . $dbi->quote($this->database) . ' AND `table_name` = ' . $dbi->quote($table_name) . ' AND `referenced_table_name` IS NOT NULL'; $dbi->setQuery($query); $allFKColumns = $dbi->loadObjectList(); $rawConstraints = array(); if (!empty($allFKColumns)) { foreach ($allFKColumns as $oColumn) { if (!array_key_exists($oColumn->CONSTRAINT_NAME, $rawConstraints)) { $entry = array( 'name' => $oColumn->CONSTRAINT_NAME, 'cols' => array(), 'refcols' => array(), 'reftable' => '', 'update' => '', 'delete' => '', ); if ($useabstract) { $entry['name'] = $this->getAbstract($entry['name']); } if (array_key_exists($oColumn->CONSTRAINT_NAME, $foreignKeyInfo)) { $entry['update'] = $foreignKeyInfo[$oColumn->CONSTRAINT_NAME]->UPDATE_RULE; $entry['delete'] = $foreignKeyInfo[$oColumn->CONSTRAINT_NAME]->DELETE_RULE; $reftable = $foreignKeyInfo[$oColumn->CONSTRAINT_NAME]->REFERENCED_TABLE_NAME; // Add a reference hit $this->dependencies[$reftable][] = $table_name; // Add the dependency to this table's metadata $dependencies[] = $reftable; if ($useabstract) { $reftable = $this->getAbstract($reftable); } $entry['reftable'] = $reftable; } $rawConstraints[$oColumn->CONSTRAINT_NAME] = $entry; } $rawConstraints[$oColumn->CONSTRAINT_NAME]['cols'][] = '`' . $oColumn->COLUMN_NAME . '`'; $rawConstraints[$oColumn->CONSTRAINT_NAME]['refcols'][] = '`' . $oColumn->REFERENCED_COLUMN_NAME . '`'; } } // Piece together the constraints' SQL statements if (!empty($rawConstraints)) { foreach ($rawConstraints as $keydef) { $line = ' CONSTRAINT '; if ($keydef['name']) { $line .= "`{$keydef['name']}` "; } $line .= 'FOREIGN KEY ('; $line .= implode(',', $keydef['cols']); $line .= ') REFERENCES `' . $keydef['reftable'] . '` ('; $line .= implode(',', $keydef['refcols']); $line .= ')'; if ($keydef['delete']) { $line .= ' ON DELETE ' . $keydef['delete']; } if ($keydef['update']) { $line .= ' ON UPDATE ' . $keydef['update']; } $constraints_sql[] = $line; } } // ===================================================================== // ========== CONSTRUCT THE TABLE CREATE STATEMENT // ===================================================================== // Create the SQL output if ($useabstract) { $table_sql = "CREATE TABLE `$table_abstract` ("; } else { $table_sql = "CREATE TABLE `$table_name` ("; } $table_sql .= implode(',', $columns_sql); if (count($keys_sql)) { $table_sql .= ',' . implode(',', $keys_sql); } if (count($constraints_sql)) { $table_sql .= ',' . implode(',', $constraints_sql); } $table_sql .= ")"; // Engine and stuff must also be exported here if ($table_object->ENGINE) { $table_sql .= ' ENGINE=' . $table_object->ENGINE; } if ($table_object->TABLE_COLLATION) { $table_sql .= ' DEFAULT COLLATE ' . $table_object->TABLE_COLLATION; } $table_sql .= ";\n"; return $table_sql; } /** * Reverse engineers the View definitions of this database * * @param DriverBase $dbi Database connection to INFORMATION_SCHEMA */ protected function reverse_engineer_views(&$dbi) { $schema_name = $this->database; $sql = 'SELECT * FROM `views` WHERE `table_schema` = ' . $dbi->quote($schema_name); $dbi->setQuery($sql); $all_views = $dbi->loadObjectList(); $registry = Factory::getConfiguration(); $root = $registry->get('volatile.database.root', '[SITEDB]'); // If we have filters, make sure the tables pass the filtering $filters = Factory::getFilters(); // First pass: populate the table_name_map if (!empty($all_views)) { foreach ($all_views as $table_object) { // Extract the table name $table_name = $table_object->TABLE_NAME; // Filter and convert if (substr($table_name, 0, 3) == '#__') { $this->setWarning(__CLASS__ . " :: Table $table_name has a prefix of #__. This would cause restoration errors; table skipped."); continue; } $table_abstract = $this->getAbstract($table_name); if (substr($table_abstract, 0, 4) != 'bak_') // Skip backup tables { // Apply exclusion filters if (!$filters->isFiltered($table_abstract, $root, 'dbobject', 'all')) { Factory::getLog()->log(LogLevel::INFO, __CLASS__ . " :: Adding $table_name (internal name $table_abstract)"); $this->table_name_map[$table_name] = $table_abstract; } else { Factory::getLog()->log(LogLevel::INFO, __CLASS__ . " :: Skipping $table_name (internal name $table_abstract)"); continue; } } else { Factory::getLog()->log(LogLevel::INFO, __CLASS__ . " :: Backup table $table_name automatically skipped."); continue; } } } // Second pass: get the create commands if (!empty($all_views)) { foreach ($all_views as $table_object) { // Extract the table name $table_name = $table_object->TABLE_NAME; if (!in_array($table_name, $this->table_name_map)) { // Skip any views which have been filtered out continue; } $table_abstract = $this->getAbstract($table_name); // Still here? The view is added. We now have to store its // create command, dependency info and so on $new_entry = array( 'type' => 'view', 'dump_records' => false ); $dependencies = array(); $table_sql = 'CREATE OR REPLACE VIEW `' . $table_name . '` AS ' . $table_object->VIEW_DEFINITION; $old_table_sql = $table_sql; foreach ($this->table_name_map as $ref_normal => $ref_abstract) { if ($pos = strpos($table_sql, "`$ref_normal`")) { // Add a reference hit $this->dependencies[$ref_normal][] = $table_name; // Add the dependency to this table's metadata $dependencies[] = $ref_normal; // Do the replacement $table_sql = str_replace("`$ref_normal`", "`$ref_abstract`", $table_sql); } } // On DB only backup we don't want any replacing to take place, do we? if (!Factory::getEngineParamsProvider()->getScriptingParameter('db.abstractnames', 1)) { $table_sql = $old_table_sql; } // Replace newlines with spaces $table_sql = str_replace("\n", " ", $table_sql) . ";\n"; $table_sql = str_replace("\r", " ", $table_sql); $table_sql = str_replace("\t", " ", $table_sql); $new_entry['create'] = $table_sql; $new_entry['dependencies'] = $dependencies; $this->tables_data[$table_name] = $new_entry; } } } }