%PDF- %PDF-
| Direktori : /home1/lightco1/www/administrator/components/com_akeeba/BackupEngine/Dump/Native/ |
| Current File : //home1/lightco1/www/administrator/components/com_akeeba/BackupEngine/Dump/Native/Mysql.php |
<?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\Native;
// Protection against direct access
defined('AKEEBAENGINE') or die();
use Akeeba\Engine\Dump\Base;
use Akeeba\Engine\Factory;
use Psr\Log\LogLevel;
/**
* A generic MySQL database dump class.
* Now supports views; merge, in-memory, federated, blackhole, etc tables
* 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 Base
{
/**
* Return the current database name by querying the database connection object (e.g. SELECT DATABASE() in MySQL)
*
* @return string
*/
protected function getDatabaseNameFromConnection()
{
$db = $this->getDB();
try
{
$ret = $db->setQuery('SELECT DATABASE()')->loadResult();
}
catch (\Exception $e)
{
return '';
}
return empty($ret) ? '' : $ret;
}
/**
* The primary key structure of the currently backed up table. The keys contained are:
* - table The name of the table being backed up
* - field The name of the primary key field
* - value The last value of the PK field
*
* @var array
*/
protected $table_autoincrement = array(
'table' => null,
'field' => null,
'value' => null,
);
/**
* Implements the constructor of the class
*
* @return Mysql
*/
public function __construct()
{
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: New instance");
}
/**
* Applies the SQL compatibility setting
*
* @return void
*/
protected function enforceSQLCompatibility()
{
$db = $this->getDB();
if ($this->getError())
{
return;
}
// Try to enforce SQL_BIG_SELECTS option
try
{
$db->setQuery('SET sql_big_selects=1');
$db->query();
}
catch (\Exception $e)
{
// Do nothing; some versions of MySQL don't allow you to use the BIG_SELECTS option.
}
$db->resetErrors();
}
/**
* Performs one more step of dumping database data
*
* @return void
*/
protected function stepDatabaseDump()
{
// Initialize local variables
$db = $this->getDB();
if ($this->getError())
{
return;
}
if (!is_object($db) || ($db === false))
{
$this->setError(__CLASS__ . '::_run() Could not connect to database?!');
return;
}
$outData = ''; // Used for outputting INSERT INTO commands
$this->enforceSQLCompatibility(); // Apply MySQL compatibility option
if ($this->getError())
{
return;
}
// Touch SQL dump file
$nada = "";
$this->writeline($nada);
// Get this table's information
$tableName = $this->nextTable;
$this->setStep($tableName);
$this->setSubstep('');
$tableAbstract = trim($this->table_name_map[ $tableName ]);
$dump_records = $this->tables_data[ $tableName ]['dump_records'];
// Restore any previously information about the largest query we had to run
$this->largest_query = Factory::getConfiguration()->get('volatile.database.largest_query', 0);
// If it is the first run, find number of rows and get the CREATE TABLE command
if ($this->nextRange == 0)
{
if ($this->getError())
{
return;
}
$outCreate = '';
if (is_array($this->tables_data[ $tableName ]))
{
if (array_key_exists('create', $this->tables_data[ $tableName ]))
{
$outCreate = $this->tables_data[ $tableName ]['create'];
}
}
if (empty($outCreate) && !empty($tableName))
{
// The CREATE command wasn't cached. Time to create it. The $type and $dependencies
// variables will be thrown away.
$type = isset($this->tables_data[ $tableName ]['type']) ? $this->tables_data[ $tableName ]['type'] : 'table';
$dependencies = array();
$outCreate = $this->get_create($tableAbstract, $tableName, $type, $dependencies);
}
// Create drop statements if required (the key is defined by the scripting engine)
if (Factory::getEngineParamsProvider()->getScriptingParameter('db.dropstatements', 0))
{
if (array_key_exists('create', $this->tables_data[ $tableName ]))
{
$dropStatement = $this->createDrop($this->tables_data[ $tableName ]['create']);
}
else
{
$type = 'table';
$createStatement = $this->get_create($tableAbstract, $tableName, $type, $dependencies);
$dropStatement = $this->createDrop($createStatement);
}
if (!empty($dropStatement))
{
$dropStatement .= "\n";
if (!$this->writeDump($dropStatement))
{
return;
}
}
}
// Write the CREATE command after any DROP command which might be necessary.
if (!$this->writeDump($outCreate))
{
return;
}
if ($dump_records)
{
// We are dumping data from a table, get the row count
$this->getRowCount($tableAbstract);
// If we can't get the row count we cannot back up this table's data
if (is_null($this->maxRange))
{
$dump_records = false;
}
}
else
{
/**
* Do NOT move this line to the if-block below. We need to only log this message on tables which are
* filtered, not on tables we simply cannot get the row count information for!
*/
Factory::getLog()->log(LogLevel::INFO, "Skipping dumping data of " . $tableAbstract);
}
// The table is either filtered or we cannot get the row count. Either way we should not dump any data.
if (!$dump_records)
{
$this->maxRange = 0;
$this->nextRange = 1;
$outData = '';
$numRows = 0;
$dump_records = false;
}
// Output any data preamble commands, e.g. SET IDENTITY_INSERT for SQL Server
if ($dump_records && Factory::getEngineParamsProvider()->getScriptingParameter('db.dropstatements', 0))
{
Factory::getLog()->log(LogLevel::DEBUG, "Writing data dump preamble for " . $tableAbstract);
$preamble = $this->getDataDumpPreamble($tableAbstract, $tableName, $this->maxRange);
if (!empty($preamble))
{
if (!$this->writeDump($preamble))
{
return;
}
}
}
// Get the table's auto increment information
if ($dump_records)
{
$this->setAutoIncrementInfo();
}
}
// Load the active database root
$configuration = Factory::getConfiguration();
$dbRoot = $configuration->get('volatile.database.root', '[SITEDB]');
// Get the default and the current (optimal) batch size
$defaultBatchSize = $this->getDefaultBatchSize();
$batchSize = $configuration->get('volatile.database.batchsize', $defaultBatchSize);
// Check if we have more work to do on this table
if (($this->nextRange < $this->maxRange))
{
$timer = Factory::getTimer();
// Get the number of rows left to dump from the current table
$sql = $db->getQuery(true)
->select('*')
->from($db->nameQuote($tableAbstract));
if (!is_null($this->table_autoincrement['field']))
{
$sql->order($db->qn($this->table_autoincrement['field']) . ' ASC');
}
if ($this->nextRange == 0)
{
// Get the optimal batch size for this table and save it to the volatile data
$batchSize = $this->getOptimalBatchSize($tableAbstract, $defaultBatchSize);
$configuration->set('volatile.database.batchsize', $batchSize);
// First run, get a cursor to all records
$db->setQuery($sql, 0, $batchSize);
Factory::getLog()->log(LogLevel::INFO, "Beginning dump of " . $tableAbstract);
Factory::getLog()->log(LogLevel::DEBUG, "Up to $batchSize records will be read at once.");
}
else
{
// Subsequent runs, get a cursor to the rest of the records
$this->setSubstep($this->nextRange . ' / ' . $this->maxRange);
// If we have an auto_increment value and the table has over $batchsize records use the indexed select instead of a plain limit
if (!is_null($this->table_autoincrement['field']) && !is_null($this->table_autoincrement['value']))
{
Factory::getLog()
->log(LogLevel::INFO, "Continuing dump of " . $tableAbstract . " from record #{$this->nextRange} using auto_increment column {$this->table_autoincrement['field']} and value {$this->table_autoincrement['value']}");
$sql->where($db->qn($this->table_autoincrement['field']) . ' > ' . $db->q($this->table_autoincrement['value']));
$db->setQuery($sql, 0, $batchSize);
}
else
{
Factory::getLog()
->log(LogLevel::INFO, "Continuing dump of " . $tableAbstract . " from record #{$this->nextRange}");
$db->setQuery($sql, $this->nextRange, $batchSize);
}
}
$this->query = '';
$numRows = 0;
$use_abstract = Factory::getEngineParamsProvider()->getScriptingParameter('db.abstractnames', 1);
$filters = Factory::getFilters();
$mustFilter = $filters->hasFilterType('dbobject', 'children');
try
{
$cursor = $db->query();
}
catch (\Exception $exc)
{
// Issue a warning about the failure to dump data
$errno = $exc->getCode();
$error = $exc->getMessage();
$this->setWarning("Failed dumping $tableAbstract from record #{$this->nextRange}. MySQL error $errno: $error");
// Reset the database driver's state (we will try to dump other tables anyway)
$db->resetErrors();
$cursor = null;
// Mark this table as done since we are unable to dump it.
$this->nextRange = $this->maxRange;
}
while (is_array($myRow = $db->fetchAssoc()) && ($numRows < ($this->maxRange - $this->nextRange)))
{
if ($this->createNewPartIfRequired() == false)
{
/**
* When createNewPartIfRequired returns false it means that we have began adding a SQL part to the
* backup archive but it hasn't finished. If we don't return here, the code below will keep adding
* data to that dump file. Yes, despite being closed. When you call writeDump the file is reopened.
* As a result of writing data of length Y, the file that had a size X now has a size of X + Y. This
* means that the loop in BaseArchiver which tries to add it to the archive will never see its End
* Of File since we are trying to resume the backup from *beyond* the file position that was
* recorded as the file size. The archive can detect a file shrinking but not a file growing!
* Therefore we hit an infinite loop a.k.a. runaway backup.
*/
return;
}
$numRows ++;
$numOfFields = count($myRow);
// On MS SQL Server there's always a RowNumber pseudocolumn added at the end, screwing up the backup (GRRRR!)
if ($db->getDriverType() == 'mssql')
{
$numOfFields --;
}
// If row-level filtering is enabled, please run the filtering
if ($mustFilter)
{
$isFiltered = $filters->isFiltered(
array(
'table' => $tableAbstract,
'row' => $myRow
),
$dbRoot,
'dbobject',
'children'
);
if ($isFiltered)
{
// Update the auto_increment value to avoid edge cases when the batch size is one
if (!is_null($this->table_autoincrement['field']) && isset($myRow[ $this->table_autoincrement['field'] ]))
{
$this->table_autoincrement['value'] = $myRow[ $this->table_autoincrement['field'] ];
}
continue;
}
}
if (
(!$this->extendedInserts) || // Add header on simple INSERTs, or...
($this->extendedInserts && empty($this->query)) //...on extended INSERTs if there are no other data, yet
)
{
$newQuery = true;
$fieldList = $this->getFieldListSQL(array_keys($myRow), $numOfFields);
if ($numOfFields > 0)
{
$this->query = "INSERT INTO " . $db->nameQuote((!$use_abstract ? $tableName : $tableAbstract)) . " $fieldList VALUES ";
}
}
else
{
// On other cases, just mark that we should add a comma and start a new VALUES entry
$newQuery = false;
}
$outData = '(';
// Step through each of the row's values
$fieldID = 0;
// Used in running backup fix
$isCurrentBackupEntry = false;
// Fix 1.2a - NULL values were being skipped
if ($numOfFields > 0)
{
foreach ($myRow as $fieldName => $value)
{
// The ID of the field, used to determine placement of commas
$fieldID ++;
if ($fieldID > $numOfFields)
{
// This is required for SQL Server backups, do NOT remove!
continue;
}
// Fix 2.0: Mark currently running backup as successful in the DB snapshot
if ($tableAbstract == '#__ak_stats')
{
if ($fieldID == 1)
{
// Compare the ID to the currently running
$statistics = Factory::getStatistics();
$isCurrentBackupEntry = ($value == $statistics->getId());
}
elseif ($fieldID == 6)
{
// Treat the status field
$value = $isCurrentBackupEntry ? 'complete' : $value;
}
}
// Post-process the value
if (is_null($value))
{
$outData .= "NULL"; // Cope with null values
}
else
{
// Accommodate for runtime magic quotes
if (function_exists('get_magic_quotes_runtime'))
{
$value = @get_magic_quotes_runtime() ? stripslashes($value) : $value;
}
$value = $db->quote($value);
if ($this->postProcessValues)
{
$value = $this->postProcessQuotedValue($value);
}
$outData .= $value;
}
if ($fieldID < $numOfFields)
{
$outData .= ', ';
}
}
}
$outData .= ')';
if ($numOfFields)
{
// If it's an existing query and we have extended inserts
if ($this->extendedInserts && !$newQuery)
{
// Check the existing query size
$query_length = strlen($this->query);
$data_length = strlen($outData);
if (($query_length + $data_length) > $this->packetSize)
{
// We are about to exceed the packet size. Write the data so far.
$this->query .= ";\n";
if (!$this->writeDump($this->query))
{
return;
}
// Then, start a new query
$this->query = '';
$this->query = "INSERT INTO " . $db->nameQuote((!$use_abstract ? $tableName : $tableAbstract)) . " VALUES ";
$this->query .= $outData;
}
else
{
// We have room for more data. Append $outData to the query.
$this->query .= ', ';
$this->query .= $outData;
}
}
// If it's a brand new insert statement in an extended INSERTs set
elseif ($this->extendedInserts && $newQuery)
{
// Append the data to the INSERT statement
$this->query .= $outData;
// Let's see the size of the dumped data...
$query_length = strlen($this->query);
if ($query_length >= $this->packetSize)
{
// This was a BIG query. Write the data to disk.
$this->query .= ";\n";
if (!$this->writeDump($this->query))
{
return;
}
// Then, start a new query
$this->query = '';
}
}
// It's a normal (not extended) INSERT statement
else
{
// Append the data to the INSERT statement
$this->query .= $outData;
// Write the data to disk.
$this->query .= ";\n";
if (!$this->writeDump($this->query))
{
return;
}
// Then, start a new query
$this->query = '';
}
}
$outData = '';
// Update the auto_increment value to avoid edge cases when the batch size is one
if (!is_null($this->table_autoincrement['field']))
{
$this->table_autoincrement['value'] = $myRow[ $this->table_autoincrement['field'] ];
}
unset($myRow);
// Check for imminent timeout
if ($timer->getTimeLeft() <= 0)
{
Factory::getLog()
->log(LogLevel::DEBUG, "Breaking dump of $tableAbstract after $numRows rows; will continue on next step");
break;
}
}
$db->freeResult($cursor);
// Advance the _nextRange pointer
$this->nextRange += ($numRows != 0) ? $numRows : 1;
$this->setStep($tableName);
$this->setSubstep($this->nextRange . ' / ' . $this->maxRange);
}
// Finalize any pending query
// WARNING! If we do not do that now, the query will be emptied in the next operation and all
// accumulated data will go away...
if (!empty($this->query))
{
$this->query .= ";\n";
if (!$this->writeDump($this->query))
{
return;
}
$this->query = '';
}
// Check for end of table dump (so that it happens inside the same operation)
if (!($this->nextRange < $this->maxRange))
{
// Tell the user we are done with the table
Factory::getLog()->log(LogLevel::DEBUG, "Done dumping " . $tableAbstract);
// Output any data preamble commands, e.g. SET IDENTITY_INSERT for SQL Server
if ($dump_records && Factory::getEngineParamsProvider()->getScriptingParameter('db.dropstatements', 0))
{
Factory::getLog()->log(LogLevel::DEBUG, "Writing data dump epilogue for " . $tableAbstract);
$epilogue = $this->getDataDumpEpilogue($tableAbstract, $tableName, $this->maxRange);
if (!empty($epilogue))
{
if (!$this->writeDump($epilogue))
{
return;
}
}
}
if (count($this->tables) == 0)
{
// We have finished dumping the database!
Factory::getLog()->log(LogLevel::INFO, "End of database detected; flushing the dump buffers...");
$null = null;
$this->writeDump($null);
Factory::getLog()->log(LogLevel::INFO, "Database has been successfully dumped to SQL file(s)");
$this->setState('postrun');
$this->setStep('');
$this->setSubstep('');
$this->nextTable = '';
$this->nextRange = 0;
// At the end of the database dump, if any query was longer than 1Mb, let's put a warning file in the installation folder
if ($this->largest_query >= 1024 * 1024)
{
$archive = Factory::getArchiverEngine();
$archive->addVirtualFile('large_tables_detected', $this->installerSettings->installerroot, $this->largest_query);
}
}
elseif (count($this->tables) != 0)
{
// Switch tables
$this->nextTable = array_shift($this->tables);
$this->nextRange = 0;
$this->setStep($this->nextTable);
$this->setSubstep('');
}
}
}
/**
* Gets the row count for table $tableAbstract. Also updates the $this->maxRange variable.
*
* @param string $tableAbstract The abstract name of the table (works with canonical names too, though)
*
* @return void
*/
protected function getRowCount($tableAbstract)
{
$db = $this->getDB();
if ($this->getError())
{
return;
}
$sql = $db->getQuery(true)
->select('COUNT(*)')
->from($db->nameQuote($tableAbstract));
$errno = 0;
$error = '';
try
{
$db->setQuery($sql);
$this->maxRange = $db->loadResult();
if (is_null($this->maxRange))
{
$errno = $db->getErrorNum();
$error = $db->getErrorMsg(false);
}
}
catch (\Exception $e)
{
$this->maxRange = null;
$errno = $e->getCode();
$error = $e->getMessage();
}
if (is_null($this->maxRange))
{
$this->setWarning("Cannot get number of rows of $tableAbstract. MySQL error $errno: $error");
return;
}
Factory::getLog()->log(LogLevel::DEBUG, "Rows on " . $tableAbstract . " : " . $this->maxRange);
}
// =============================================================================
// Dependency processing - the Twilight Zone starts here
// =============================================================================
/**
* Scans the database for tables to be backed up and sorts them according to
* their dependencies on one another. Updates $this->dependencies.
*
* @return void
*/
protected function getTablesToBackup()
{
// Makes the MySQL connection compatible with our class
$this->enforceSQLCompatibility();
$configuration = Factory::getConfiguration();
$notracking = $configuration->get('engine.dump.native.nodependencies', 0);
// First, get a map of table names <--> abstract names
$this->get_tables_mapping();
if ($this->getError())
{
return;
}
if ($notracking)
{
// Do not process table & view dependencies
$this->get_tables_data_without_dependencies();
if ($this->getError())
{
return;
}
}
// Process table & view dependencies (default)
else
{
// Find the type and CREATE command of each table/view in the database
$this->get_tables_data();
if ($this->getError())
{
return;
}
// Process dependencies and rearrange tables respecting them
$this->process_dependencies();
if ($this->getError())
{
return;
}
// Remove dependencies array
$this->dependencies = array();
}
}
/**
* Generates a mapping between table names as they're stored in the database
* and their abstract representation. Updates $this->table_name_map
*
* @return void
*/
protected function get_tables_mapping()
{
// Get a database connection
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Finding tables to include in the backup set");
$db = $this->getDB();
if ($this->getError())
{
return;
}
// Reset internal tables
$this->table_name_map = array();
// Get the list of all database tables
$sql = "SHOW TABLES";
$db->setQuery($sql);
$all_tables = $db->loadResultArray();
$registry = Factory::getConfiguration();
$root = $registry->get('volatile.database.root', '[SITEDB]');
// If we have filters, make sure the tables pass the filtering
$filters = Factory::getFilters();
foreach ($all_tables as $table_name)
{
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)");
}
}
else
{
Factory::getLog()->log(LogLevel::INFO, __CLASS__ . " :: Backup table $table_name automatically skipped.");
}
}
// 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.
$enable_entities = $registry->get('engine.dump.native.advanced_entitites', true);
$notracking = $registry->get('engine.dump.native.nodependencies', 0);
if (!$enable_entities)
{
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: NOT listing stored PROCEDUREs, FUNCTIONs and TRIGGERs (you told me not to)");
}
elseif ($notracking != 0)
{
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: NOT listing stored PROCEDUREs, FUNCTIONs and TRIGGERs (you have disabled dependency tracking, therefore I can't handle advanced entities)");
}
if ($enable_entities && ($notracking == 0))
{
// Cache the database name if this is the main site's database
// 1. Stored procedures
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Listing stored PROCEDUREs");
$sql = "SHOW PROCEDURE STATUS WHERE `Db`=" . $db->quote($this->database);
$db->setQuery($sql);
try
{
$all_entries = $db->loadResultArray(1);
}
catch (\Exception $e)
{
$all_entries = array();
}
// If we have filters, make sure the tables pass the filtering
if (is_array($all_entries))
{
if (count($all_entries))
{
foreach ($all_entries as $entity_name)
{
$entity_abstract = $this->getAbstract($entity_name);
if (!(substr($entity_abstract, 0, 4) == 'bak_')) // Skip backup entities
{
if (!$filters->isFiltered($entity_abstract, $root, 'dbobject', 'all'))
{
$this->table_name_map[$entity_name] = $entity_abstract;
}
}
}
}
}
// 2. Stored functions
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Listing stored FUNCTIONs");
$sql = "SHOW FUNCTION STATUS WHERE `Db`=" . $db->quote($this->database);
$db->setQuery($sql);
try
{
$all_entries = $db->loadResultArray(1);
}
catch (\Exception $e)
{
$all_entries = array();
}
// If we have filters, make sure the tables pass the filtering
if (is_array($all_entries))
{
if (count($all_entries))
{
foreach ($all_entries as $entity_name)
{
$entity_abstract = $this->getAbstract($entity_name);
if (!(substr($entity_abstract, 0, 4) == 'bak_')) // Skip backup entities
{
// Apply exclusion filters if set
if (!$filters->isFiltered($entity_abstract, $root, 'dbobject', 'all'))
{
$this->table_name_map[$entity_name] = $entity_abstract;
}
}
}
}
}
// 3. Triggers
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Listing stored TRIGGERs");
$sql = "SHOW TRIGGERS";
$db->setQuery($sql);
try
{
$all_entries = $db->loadResultArray();
}
catch (\Exception $e)
{
$all_entries = array();
}
// If we have filters, make sure the tables pass the filtering
if (is_array($all_entries))
{
if (count($all_entries))
{
foreach ($all_entries as $entity_name)
{
$entity_abstract = $this->getAbstract($entity_name);
if (!(substr($entity_abstract, 0, 4) == 'bak_')) // Skip backup entities
{
// Apply exclusion filters if set
if (!$filters->isFiltered($entity_abstract, $root, 'dbobject', 'all'))
{
$this->table_name_map[$entity_name] = $entity_abstract;
}
}
}
}
}
} // if MySQL 5
}
/**
* Populates the _tables array with the metadata of each table and generates
* dependency information for views and merge tables. Updates $this->tables_data.
*
* @return void
*/
protected function get_tables_data()
{
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Starting CREATE TABLE and dependency scanning");
// Get a database connection
$db = $this->getDB();
if ($this->getError())
{
return;
}
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Got database connection");
// Reset internal tables
$this->tables_data = array();
$this->dependencies = array();
// Get a list of tables where their engine type is shown
$sql = 'SHOW TABLES';
$db->setQuery($sql);
$metadata_list = $db->loadRowList();
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Got SHOW TABLES");
// Get filters and filter root
$registry = Factory::getConfiguration();
$root = $registry->get('volatile.database.root', '[SITEDB]');
$filters = Factory::getFilters();
foreach ($metadata_list as $table_metadata)
{
// Skip over tables not included in the backup set
if (!array_key_exists($table_metadata[0], $this->table_name_map))
{
continue;
}
// Basic information
$table_name = $table_metadata[0];
$table_abstract = $this->table_name_map[$table_metadata[0]];
$new_entry = array(
'type' => 'table',
'dump_records' => true
);
// Get the CREATE command
$dependencies = array();
$new_entry['create'] = $this->get_create($table_abstract, $table_name, $new_entry['type'], $dependencies);
$new_entry['dependencies'] = $dependencies;
if ($new_entry['type'] == 'view')
{
$new_entry['dump_records'] = false;
}
else
{
$new_entry['dump_records'] = true;
}
// Scan for the table engine.
$engine = null; // So that we detect VIEWs correctly
if ($new_entry['type'] == 'table')
{
$engine = 'MyISAM'; // So that even with MySQL 4 hosts we don't screw this up
$engine_keys = array('ENGINE=', 'TYPE=');
foreach ($engine_keys as $engine_key)
{
$start_pos = strrpos($new_entry['create'], $engine_key);
if ($start_pos !== false)
{
// Advance the start position just after the position of the ENGINE keyword
$start_pos += strlen($engine_key);
// Try to locate the space after the engine type
$end_pos = stripos($new_entry['create'], ' ', $start_pos);
if ($end_pos === false)
{
// Uh... maybe it ends with ENGINE=EngineType;
$end_pos = stripos($new_entry['create'], ';', $start_pos);
}
if ($end_pos !== false)
{
// Grab the string
$engine = substr($new_entry['create'], $start_pos, $end_pos - $start_pos);
if (empty($engine))
{
Factory::getLog()->log(LogLevel::DEBUG, "*** DEBUG *** $table_name - engine $engine");
Factory::getLog()->log(LogLevel::DEBUG, $new_entry['create']);
Factory::getLog()->log(LogLevel::DEBUG, "start $start_pos - end $end_pos");
}
}
}
}
$engine = strtoupper($engine);
}
switch ($engine)
{
/*
// Views -- They are detected based on their CREATE statement
case null:
$new_entry['type'] = 'view';
$new_entry['dump_records'] = false;
break;
*/
// 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 and VIEWs
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;
}
$this->tables_data[$table_name] = $new_entry;
}
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Got table list");
// If we have MySQL > 5.0 add stored procedures, stored functions and triggers
$enable_entities = $registry->get('engine.dump.native.advanced_entitites', true);
if ($enable_entities)
{
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Listing MySQL entities");
// Get a list of procedures
$sql = 'SHOW PROCEDURE STATUS WHERE `Db`=' . $db->quote($this->database);
$db->setQuery($sql);
try
{
$metadata_list = $db->loadRowList();
}
catch (\Exception $e)
{
$metadata_list = null;
}
if (is_array($metadata_list))
{
if (count($metadata_list))
{
foreach ($metadata_list as $entity_metadata)
{
// Skip over entities not included in the backup set
if (!array_key_exists($entity_metadata[1], $this->table_name_map))
{
continue;
}
// Basic information
$entity_name = $entity_metadata[1];
$entity_abstract = $this->table_name_map[$entity_metadata[1]];
$new_entry = array(
'type' => 'procedure',
'dump_records' => false
);
// There's no point trying to add a non-procedure entity
if ($entity_metadata[2] != 'PROCEDURE')
{
continue;
}
$dependencies = array();
$new_entry['create'] = $this->get_create($entity_abstract, $entity_name, $new_entry['type'], $dependencies);
$new_entry['dependencies'] = $dependencies;
$this->tables_data[$entity_name] = $new_entry;
}
}
} // foreach
// Get a list of functions
$sql = 'SHOW FUNCTION STATUS WHERE `Db`=' . $db->quote($this->database);
$db->setQuery($sql);
try
{
$metadata_list = $db->loadRowList();
}
catch (\Exception $e)
{
$metadata_list = null;
}
if (is_array($metadata_list))
{
if (count($metadata_list))
{
foreach ($metadata_list as $entity_metadata)
{
// Skip over entities not included in the backup set
if (!array_key_exists($entity_metadata[1], $this->table_name_map))
{
continue;
}
// Basic information
$entity_name = $entity_metadata[1];
$entity_abstract = $this->table_name_map[$entity_metadata[1]];
$new_entry = array(
'type' => 'function',
'dump_records' => false
);
// There's no point trying to add a non-function entity
if ($entity_metadata[2] != 'FUNCTION')
{
continue;
}
$dependencies = array();
$new_entry['create'] = $this->get_create($entity_abstract, $entity_name, $new_entry['type'], $dependencies);
$new_entry['dependencies'] = $dependencies;
$this->tables_data[$entity_name] = $new_entry;
}
}
} // foreach
// Get a list of triggers
$sql = 'SHOW TRIGGERS';
$db->setQuery($sql);
try
{
$metadata_list = $db->loadRowList();
}
catch (\Exception $e)
{
$metadata_list = null;
}
if (is_array($metadata_list))
{
if (count($metadata_list))
{
foreach ($metadata_list as $entity_metadata)
{
// Skip over entities not included in the backup set
if (!array_key_exists($entity_metadata[0], $this->table_name_map))
{
continue;
}
// Basic information
$entity_name = $entity_metadata[0];
$entity_abstract = $this->table_name_map[$entity_metadata[0]];
$new_entry = array(
'type' => 'trigger',
'dump_records' => false
);
$dependencies = array();
$new_entry['create'] = $this->get_create($entity_abstract, $entity_name, $new_entry['type'], $dependencies);
$new_entry['dependencies'] = $dependencies;
$this->tables_data[$entity_name] = $new_entry;
}
}
} // foreach
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Got MySQL entities list");
}
/**
// Only store unique values
if(count($dependencies) > 0)
$dependencies = array_unique($dependencies);
/**/
}
/**
* Populates the _tables array with the metadata of each table.
* Updates $this->tables_data and $this->tables.
*
* @return void
*/
protected function get_tables_data_without_dependencies()
{
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Pushing table data (without dependency tracking)");
// Reset internal tables
$this->tables_data = array();
$this->dependencies = array();
// Get filters and filter root
$registry = Factory::getConfiguration();
$root = $registry->get('volatile.database.root', '[SITEDB]');
$filters = Factory::getFilters();
foreach ($this->table_name_map as $table_name => $table_abstract)
{
$new_entry = array(
'type' => 'table',
'dump_records' => true
);
// Table Data Filter - skip dumping table contents of filtered out tables
if ($filters->isFiltered($table_abstract, $root, 'dbobject', 'content'))
{
$new_entry['dump_records'] = false;
}
$this->tables_data[$table_name] = $new_entry;
$this->tables[] = $table_name;
} // foreach
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Got table list");
}
/**
* Gets the CREATE TABLE command for a given table/view/procedure/function/trigger
*
* @param string $table_abstract The abstracted name of the entity
* @param string $table_name The name of the table
* @param string $type The type of the entity to scan. If it's found to differ, the correct type is returned.
* @param array $dependencies The dependencies of this table
*
* @return string The CREATE command, w/out newlines
*/
protected function get_create($table_abstract, $table_name, &$type, &$dependencies)
{
$configuration = Factory::getConfiguration();
$notracking = $configuration->get('engine.dump.native.nodependencies', 0);
$db = $this->getDB();
if ($this->getError())
{
return;
}
switch ($type)
{
case 'table':
case 'merge':
case 'view':
default:
$sql = "SHOW CREATE TABLE `$table_abstract`";
break;
case 'procedure':
$sql = "SHOW CREATE PROCEDURE `$table_abstract`";
break;
case 'function':
$sql = "SHOW CREATE FUNCTION `$table_abstract`";
break;
case 'trigger':
$sql = "SHOW CREATE TRIGGER `$table_abstract`";
break;
}
$db->setQuery($sql);
try
{
$temp = $db->loadRowList();
}
catch (\Exception $e)
{
// If the query failed we don't have the necessary SHOW privilege. Log the error and fake an empty reply.
$entityType = ($type == 'merge') ? 'table' : $type;
$msg = $e->getMessage();
$this->setWarning("Cannot get the structure of $entityType $table_abstract. Database returned error $msg running $sql Please check your database privileges. Your database backup may be incomplete.");
$db->resetErrors();
$temp = array(
array('', '', '')
);
}
if (in_array($type, array('procedure', 'function', 'trigger')))
{
$table_sql = $temp[0][2];
// MySQL adds the database name into everything. We have to remove it.
$dbName = $db->qn($this->database) . '.`';
$table_sql = str_replace($dbName, '`', $table_sql);
// These can contain comment lines, starting with a double dash. Remove them.
$table_sql = $this->removeMySQLComments($table_sql);
$table_sql = trim($table_sql);
$lines = explode("\n", $table_sql);
$lines = array_map('trim', $lines);
$table_sql = implode(' ', $lines);
$table_sql = trim($table_sql);
/**
* Remove the definer from the CREATE PROCEDURE/TRIGGER/FUNCTION. For example, MySQL returns this:
* CREATE DEFINER=`myuser`@`localhost` PROCEDURE `abc_myProcedure`() ...
* If you're restoring on a different machine the definer will probably be invalid, therefore we need to
* remove it from the (portable) output.
*/
$pattern = '/^CREATE(.*) ' . strtoupper($type) . ' (.*)/i';
$result = preg_match($pattern, $table_sql, $matches);
$table_sql = 'CREATE ' . strtoupper($type) . ' ' . $matches[2];
if (substr($table_sql, -1) != ';')
{
$table_sql .= ';';
}
}
else
{
$table_sql = $temp[0][1];
}
unset($temp);
// Smart table type detection
if (in_array($type, array('table', 'merge', 'view')))
{
// Check for CREATE VIEW
$pattern = '/^CREATE(.*) VIEW (.*)/i';
$result = preg_match($pattern, $table_sql, $matches);
if ($result === 1)
{
// This is a view.
$type = 'view';
/**
* Newer MySQL versions add the definer and other information in the CREATE VIEW output, e.g.
* CREATE ALGORITHM=UNDEFINED DEFINER=`muyser`@`localhost` SQL SECURITY DEFINER VIEW `abc_myview` AS ...
* We need to remove that to prevent restoration troubles.
*/
$table_sql = 'CREATE VIEW ' . $matches[2];
}
else
{
// This is a table.
$type = 'table';
// # Fix 3.2.1: USING BTREE / USING HASH in indices causes issues migrating from MySQL 5.1+ hosts to
// MySQL 5.0 hosts
if ($configuration->get('engine.dump.native.nobtree', 1))
{
$table_sql = str_replace(' USING BTREE', ' ', $table_sql);
$table_sql = str_replace(' USING HASH', ' ', $table_sql);
}
// Translate TYPE= to ENGINE=
$table_sql = str_replace('TYPE=', 'ENGINE=', $table_sql);
}
// Is it a VIEW but we don't have SHOW VIEW privileges?
if (empty($table_sql))
{
$type = 'view';
}
}
/**
* Replace table name and names of referenced tables with their abstracted forms and populate dependency tables
* at the same time.
*/
// On DB only backup we don't want any replacing to take place, do we?
if (!Factory::getEngineParamsProvider()->getScriptingParameter('db.abstractnames', 1))
{
$old_table_sql = $table_sql;
}
// Replace the table name with the abstract version.
// We have to quote the table name. If we don't we'll get wrong results. Imagine that you have a column whose name starts
// with the string literal of the table name itself.
// Example: table `poll`, column `poll_id` would become #__poll, #__poll_id
// By quoting before we make sure this won't happen.
$table_sql = str_replace($db->quoteName($table_name), $db->quoteName($table_abstract), $table_sql);
// Return dependency information only if dependency tracking is enabled
if (!$notracking)
{
// Even on simple tables, we may have foreign key references.
// As a result, we need to replace those referenced table names
// as well. On views and merge arrays, we have referenced tables
// by definition.
$dependencies = array();
// Now, loop for all table entries
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);
}
}
// Finally, replace the prefix if it's not empty (used in constraints)
if (!empty($this->prefix))
{
$table_sql = str_replace('`' . $this->prefix, '`#__', $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);
/**
* Views, procedures, functions and triggers may contain the database name followed by the table name, always
* quoted e.g. `db`.`table_name` We need to replace all these instances with just the table name. The only
* reliable way to do that is to look for "`db`.`" and replace it with "`"
*/
if (in_array($type, array('view', 'procedure', 'function', 'trigger')))
{
$dbName = $db->qn($this->getDatabaseName());
$dummyQuote = $db->qn('foo');
$findWhat = $dbName . '.' . substr($dummyQuote, 0, 1);
$replaceWith = substr($dummyQuote, 0, 1);
$table_sql = str_replace($findWhat, $replaceWith, $table_sql);
}
// Post-process CREATE VIEW
if ($type == 'view')
{
$pos_view = strpos($table_sql, ' VIEW ');
if ($pos_view > 7)
{
// Only post process if there are view properties between the CREATE and VIEW keywords
$propstring = substr($table_sql, 7, $pos_view - 7); // Properties string
// Fetch the ALGORITHM={UNDEFINED | MERGE | TEMPTABLE} keyword
$algostring = '';
$algo_start = strpos($propstring, 'ALGORITHM=');
if ($algo_start !== false)
{
$algo_end = strpos($propstring, ' ', $algo_start);
$algostring = substr($propstring, $algo_start, $algo_end - $algo_start + 1);
}
// Create our modified create statement
$table_sql = 'CREATE OR REPLACE ' . $algostring . substr($table_sql, $pos_view);
}
}
elseif ($type == 'procedure')
{
$pos_entity = stripos($table_sql, ' PROCEDURE ');
if ($pos_entity !== false)
{
$table_sql = 'CREATE' . substr($table_sql, $pos_entity);
}
}
elseif ($type == 'function')
{
$pos_entity = stripos($table_sql, ' FUNCTION ');
if ($pos_entity !== false)
{
$table_sql = 'CREATE' . substr($table_sql, $pos_entity);
}
}
elseif ($type == 'trigger')
{
$pos_entity = stripos($table_sql, ' TRIGGER ');
if ($pos_entity !== false)
{
$table_sql = 'CREATE' . substr($table_sql, $pos_entity);
}
}
// Add DROP statements for DB only backup
if (Factory::getEngineParamsProvider()->getScriptingParameter('db.dropstatements', 0))
{
if (($type == 'table') || ($type == 'merge'))
{
// Defense against CVE-2016-5483 ("Bad Dump") affecting MySQL, Percona, MariaDB and other MySQL clones
$table_name = str_replace(array("\r", "\n"), array('', ''), $table_name);
// Table or merge tables, get a DROP TABLE statement
$drop = "DROP TABLE IF EXISTS " . $db->quoteName($table_name) . ";\n";
}
elseif ($type == 'view')
{
// Defense against CVE-2016-5483 ("Bad Dump") affecting MySQL, Percona, MariaDB and other MySQL clones
$table_name = str_replace(array("\r", "\n"), array('', ''), $table_name);
// Views get a DROP VIEW statement
$drop = "DROP VIEW IF EXISTS " . $db->quoteName($table_name) . ";\n";
}
elseif ($type == 'procedure')
{
// Defense against CVE-2016-5483 ("Bad Dump") affecting MySQL, Percona, MariaDB and other MySQL clones
$table_name = str_replace(array("\r", "\n"), array('', ''), $table_name);
// Procedures get a DROP PROCEDURE statement and proper delimiter strings
$drop = "DROP PROCEDURE IF EXISTS " . $db->quoteName($table_name) . ";\n";
$drop .= "DELIMITER // ";
$table_sql = str_replace("\r", " ", $table_sql);
$table_sql = str_replace("\t", " ", $table_sql);
$table_sql = rtrim($table_sql, ";\n") . " // DELIMITER ;\n";
}
elseif ($type == 'function')
{
// Defense against CVE-2016-5483 ("Bad Dump") affecting MySQL, Percona, MariaDB and other MySQL clones
$table_name = str_replace(array("\r", "\n"), array('', ''), $table_name);
// Procedures get a DROP FUNCTION statement and proper delimiter strings
$drop = "DROP FUNCTION IF EXISTS " . $db->quoteName($table_name) . ";\n";
$drop .= "DELIMITER // ";
$table_sql = str_replace("\r", " ", $table_sql);
$table_sql = rtrim($table_sql, ";\n") . "// DELIMITER ;\n";
}
elseif ($type == 'trigger')
{
// Defense against CVE-2016-5483 ("Bad Dump") affecting MySQL, Percona, MariaDB and other MySQL clones
$table_name = str_replace(array("\r", "\n"), array('', ''), $table_name);
// Procedures get a DROP TRIGGER statement and proper delimiter strings
$drop = "DROP TRIGGER IF EXISTS " . $db->quoteName($table_name) . ";\n";
$drop .= "DELIMITER // ";
$table_sql = str_replace("\r", " ", $table_sql);
$table_sql = str_replace("\t", " ", $table_sql);
$table_sql = rtrim($table_sql, ";\n") . "// DELIMITER ;\n";
}
$table_sql = $drop . $table_sql;
}
return $table_sql;
}
/**
* Process all table dependencies
*
* @return void
*/
protected function process_dependencies()
{
if (count($this->table_name_map) > 0)
{
foreach ($this->table_name_map as $table_name => $table_abstract)
{
$this->push_table($table_name);
}
}
Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . " :: Processed dependencies");
}
/**
* Pushes a table in the _tables stack, making sure it will appear after
* its dependencies and other tables/views depending on it will eventually
* appear after it. It's a complicated chicken-and-egg problem. Just make
* sure you don't have any bloody circular references!!
*
* @param string $table_name Canonical name of the table to push
* @param array $stack When called recursive, other views/tables previously processed in order to detect *ahem* dependency loops...
*
* @return void
*/
protected function push_table($table_name, $stack = array(), $currentRecursionDepth = 0)
{
// Load information
$table_data = $this->tables_data[$table_name];
if (array_key_exists('dependencies', $table_data))
{
$referenced = $table_data['dependencies'];
}
else
{
$referenced = array();
}
unset($table_data);
// Try to find the minimum insert position, so as to appear after the last referenced table
$insertpos = false;
if (count($referenced))
{
foreach ($referenced as $referenced_table)
{
if (count($this->tables))
{
$newpos = array_search($referenced_table, $this->tables);
if ($newpos !== false)
{
if ($insertpos === false)
{
$insertpos = $newpos;
}
else
{
$insertpos = max($insertpos, $newpos);
}
}
}
}
}
// Add to the _tables array
if (count($this->tables) && ($insertpos !== false))
{
array_splice($this->tables, $insertpos + 1, 0, $table_name);
}
else
{
$this->tables[] = $table_name;
}
// Here's what... Some other table/view might depend on us, so we must appear
// before it (actually, it must appear after us). So, we scan for such
// tables/views and relocate them
if (count($this->dependencies))
{
if (array_key_exists($table_name, $this->dependencies))
{
foreach ($this->dependencies[$table_name] as $depended_table)
{
// First, make sure that either there is no stack, or the
// depended table doesn't belong it. In any other case, we
// were fooled to follow an endless dependency loop and we
// will simply bail out and let the user sort things out.
if (count($stack) > 0)
{
if (in_array($depended_table, $stack))
{
continue;
}
}
$my_position = array_search($table_name, $this->tables);
$remove_position = array_search($depended_table, $this->tables);
if (($remove_position !== false) && ($remove_position < $my_position))
{
$stack[] = $table_name;
array_splice($this->tables, $remove_position, 1);
// Where should I put the other table/view now? Don't tell me.
// I have to recurse...
if ($currentRecursionDepth < 19)
{
$this->push_table($depended_table, $stack, ++$currentRecursionDepth);
}
else
{
// We're hitting a circular dependency. We'll add the removed $depended_table
// in the penultimate position of the table and cross our virtual fingers...
array_splice($this->tables, count($this->tables) - 1, 0, $depended_table);
}
}
}
}
}
}
/**
* Creates a drop query from a CREATE query
*
* @param string $query The CREATE query to process
*
* @return string The DROP statement
*/
protected function createDrop($query)
{
$db = $this->getDB();
// Initialize
$dropQuery = '';
// Parse CREATE TABLE commands
if (substr($query, 0, 12) == 'CREATE TABLE')
{
// Try to get the table name
$restOfQuery = trim(substr($query, 12, strlen($query) - 12)); // Rest of query, after CREATE TABLE
// Is there a backtick?
if (substr($restOfQuery, 0, 1) == '`')
{
// There is... Good, we'll just find the matching backtick
$pos = strpos($restOfQuery, '`', 1);
$tableName = substr($restOfQuery, 1, $pos - 1);
}
else
{
// Nope, let's assume the table name ends in the next blank character
$pos = strpos($restOfQuery, ' ', 1);
$tableName = substr($restOfQuery, 0, $pos);
}
unset($restOfQuery);
// Defense against CVE-2016-5483 ("Bad Dump") affecting MySQL, Percona, MariaDB and other MySQL clones
$tableName = str_replace(array("\r", "\n"), array('', ''), $tableName);
// Try to drop the table anyway
$dropQuery = 'DROP TABLE IF EXISTS ' . $db->nameQuote($tableName) . ';';
}
// Parse CREATE VIEW commands
elseif ((substr($query, 0, 7) == 'CREATE ') && (strpos($query, ' VIEW ') !== false))
{
// Try to get the view name
$view_pos = strpos($query, ' VIEW ');
$restOfQuery = trim(substr($query, $view_pos + 6)); // Rest of query, after VIEW string
// Is there a backtick?
if (substr($restOfQuery, 0, 1) == '`')
{
// There is... Good, we'll just find the matching backtick
$pos = strpos($restOfQuery, '`', 1);
$tableName = substr($restOfQuery, 1, $pos - 1);
}
else
{
// Nope, let's assume the table name ends in the next blank character
$pos = strpos($restOfQuery, ' ', 1);
$tableName = substr($restOfQuery, 0, $pos);
}
unset($restOfQuery);
// Defense against CVE-2016-5483 ("Bad Dump") affecting MySQL, Percona, MariaDB and other MySQL clones
$tableName = str_replace(array("\r", "\n"), array('', ''), $tableName);
$dropQuery = 'DROP VIEW IF EXISTS ' . $db->nameQuote($tableName) . ';';
}
// CREATE PROCEDURE pre-processing
elseif ((substr($query, 0, 7) == 'CREATE ') && (strpos($query, 'PROCEDURE ') !== false))
{
// Try to get the procedure name
$entity_keyword = ' PROCEDURE ';
$entity_pos = strpos($query, $entity_keyword);
$restOfQuery = trim(substr($query, $entity_pos + strlen($entity_keyword))); // Rest of query, after entity key string
// Is there a backtick?
if (substr($restOfQuery, 0, 1) == '`')
{
// There is... Good, we'll just find the matching backtick
$pos = strpos($restOfQuery, '`', 1);
$entity_name = substr($restOfQuery, 1, $pos - 1);
}
else
{
// Nope, let's assume the entity name ends in the next blank character
$pos = strpos($restOfQuery, ' ', 1);
$entity_name = substr($restOfQuery, 0, $pos);
}
unset($restOfQuery);
// Defense against CVE-2016-5483 ("Bad Dump") affecting MySQL, Percona, MariaDB and other MySQL clones
$entity_name = str_replace(array("\r", "\n"), array('', ''), $entity_name);
$dropQuery = 'DROP' . $entity_keyword . 'IF EXISTS `' . $entity_name . '`;';
}
// CREATE FUNCTION pre-processing
elseif ((substr($query, 0, 7) == 'CREATE ') && (strpos($query, 'FUNCTION ') !== false))
{
// Try to get the procedure name
$entity_keyword = ' FUNCTION ';
$entity_pos = strpos($query, $entity_keyword);
$restOfQuery = trim(substr($query, $entity_pos + strlen($entity_keyword))); // Rest of query, after entity key string
// Is there a backtick?
if (substr($restOfQuery, 0, 1) == '`')
{
// There is... Good, we'll just find the matching backtick
$pos = strpos($restOfQuery, '`', 1);
$entity_name = substr($restOfQuery, 1, $pos - 1);
}
else
{
// Nope, let's assume the entity name ends in the next blank character
$pos = strpos($restOfQuery, ' ', 1);
$entity_name = substr($restOfQuery, 0, $pos);
}
unset($restOfQuery);
// Defense against CVE-2016-5483 ("Bad Dump") affecting MySQL, Percona, MariaDB and other MySQL clones
$entity_name = str_replace(array("\r", "\n"), array('', ''), $entity_name);
// Try to drop the entity anyway
$dropQuery = 'DROP' . $entity_keyword . 'IF EXISTS `' . $entity_name . '`;';
}
// CREATE TRIGGER pre-processing
elseif ((substr($query, 0, 7) == 'CREATE ') && (strpos($query, 'TRIGGER ') !== false))
{
// Try to get the procedure name
$entity_keyword = ' TRIGGER ';
$entity_pos = strpos($query, $entity_keyword);
$restOfQuery = trim(substr($query, $entity_pos + strlen($entity_keyword))); // Rest of query, after entity key string
// Is there a backtick?
if (substr($restOfQuery, 0, 1) == '`')
{
// There is... Good, we'll just find the matching backtick
$pos = strpos($restOfQuery, '`', 1);
$entity_name = substr($restOfQuery, 1, $pos - 1);
}
else
{
// Nope, let's assume the entity name ends in the next blank character
$pos = strpos($restOfQuery, ' ', 1);
$entity_name = substr($restOfQuery, 0, $pos);
}
unset($restOfQuery);
// Defense against CVE-2016-5483 ("Bad Dump") affecting MySQL, Percona, MariaDB and other MySQL clones
$entity_name = str_replace(array("\r", "\n"), array('', ''), $entity_name);
// Try to drop the entity anyway
$dropQuery = 'DROP' . $entity_keyword . 'IF EXISTS `' . $entity_name . '`;';
}
return $dropQuery;
}
/**
* Try to find an auto_increment field for the table being currently backed up and populate the
* $this->table_autoincrement table. Updates $this->table_autoincrement.
*
* @return void
*/
protected function setAutoIncrementInfo()
{
$this->table_autoincrement = array(
'table' => $this->nextTable,
'field' => null,
'value' => null,
);
$db = $this->getDB();
$query = 'SHOW COLUMNS FROM ' . $db->qn($this->nextTable) . ' WHERE ' . $db->qn('Extra') . ' = ' .
$db->q('auto_increment') . ' AND ' . $db->qn('Null') . ' = ' . $db->q('NO');
$keyInfo = $db->setQuery($query)->loadAssocList();
if (!empty($keyInfo))
{
$row = array_shift($keyInfo);
$this->table_autoincrement['field'] = $row['Field'];
}
}
/**
* Removes MySQL comments from the SQL command
*
* @param string $sql Potentially commented SQL
*
* @return string SQL without comments
*
* @see http://stackoverflow.com/questions/9690448/regular-expression-to-remove-comments-from-sql-statement
*/
protected function removeMySQLComments($sql)
{
$sqlComments = '@(([\'"]).*?[^\\\]\2)|((?:\#|--).*?$|/\*(?:[^/*]|/(?!\*)|\*(?!/)|(?R))*\*\/)\s*|(?<=;)\s+@ms';
return preg_replace($sqlComments, '$1', $sql);
}
/**
* Get the default database dump batch size from the configuration
*
* @return int
*/
protected function getDefaultBatchSize()
{
static $batchSize = null;
if (is_null($batchSize))
{
$configuration = Factory::getConfiguration();
$batchSize = intval($configuration->get('engine.dump.common.batchsize', 1000));
if ($batchSize <= 0)
{
$batchSize = 1000;
}
}
return $batchSize;
}
/**
* Get the optimal row batch size for a given table based on the available memory
*
* @param string $tableAbstract The abstract table name, e.g. #__foobar
* @param int $defaultBatchSize The default row batch size in the application configuration
*
* @return int
*/
protected function getOptimalBatchSize($tableAbstract, $defaultBatchSize)
{
$db = $this->getDB();
try
{
$info = $db->setQuery('SHOW TABLE STATUS LIKE ' . $db->q($tableAbstract))->loadAssoc();
}
catch (\Exception $e)
{
return $defaultBatchSize;
}
if (!isset($info['Avg_row_length']) || empty($info['Avg_row_length']))
{
return $defaultBatchSize;
}
// That's the average row size as reported by MySQL.
$avgRow = str_replace(array(',', '.'), array('', ''), $info['Avg_row_length']);
// The memory available for manipulating data is less than the free memory
$memoryLimit = $this->getMemoryLimit();
$memoryLimit = empty($memoryLimit) ? 33554432 : $memoryLimit;
$usedMemory = memory_get_usage();
$memoryLeft = 0.75 * ($memoryLimit - $usedMemory);
// The 3.25 factor is empirical and leans on the safe side.
$maxRows = (int) ($memoryLeft / (3.25 * $avgRow));
return max(1, min($maxRows, $defaultBatchSize));
}
/**
* Converts a human formatted size to integer representation of bytes,
* e.g. 1M to 1024768
*
* @param string $setting The value in human readable format, e.g. "1M"
*
* @return integer The value in bytes
*/
protected function humanToIntegerBytes($setting)
{
$val = trim($setting);
$last = strtolower($val{strlen($val) - 1});
if (is_numeric($last))
{
return $setting;
}
switch ($last)
{
case 't':
$val *= 1024;
case 'g':
$val *= 1024;
case 'm':
$val *= 1024;
case 'k':
$val *= 1024;
}
return (int) $val;
}
/**
* Get the PHP memory limit in bytes
*
* @return int|null Memory limit in bytes or null if we can't figure it out.
*/
protected function getMemoryLimit()
{
if (!function_exists('ini_get'))
{
return null;
}
$memLimit = ini_get("memory_limit");
if ((is_numeric($memLimit) && ($memLimit < 0)) || !is_numeric($memLimit))
{
$memLimit = 0; // 1.2a3 -- Rare case with memory_limit < 0, e.g. -1Mb!
}
$memLimit = $this->humanToIntegerBytes($memLimit);
return $memLimit;
}
}