* @copyright 2007-2011 PrestaShop SA * @version Release: $Revision: 7744 $ * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) * International Registered Trademark & Property of PrestaShop SA */ /** * This class search Prestashop version from current database structure * * @since 1.4.2.3 * @todo (priority low) compare index keys updates ; compare types range ; add custom methods to search versions with no SQL updates */ class GetVersionFromDb { /** * @var array Because of some errors and differences between some upgrades files and some db.sql files, we need to ignore some fields. */ private $ignore = array( 'blocklink' => array( 'fields' => array('id_link', 'id_blocklink'), ), 'blocklink_lang' => array( 'fields' => array('id_link', 'id_blocklink'), 'keys' => array('primary'), ), 'cms_block' => array( 'fields' => array('id_block_cms', 'id_cms_block', 'display_store'), ), 'cms_block_lang' => array( 'fields' => array('id_block_cms', 'id_cms_block'), ), 'cms_block_page' => array( 'fields' => array('id_block_cms_page', 'id_cms_block_page'), ), 'cms_lang' => array( 'types' => array('content'), ), 'log_email' => array( 'keys' => array('date_add', 'id_cart'), ), 'newsletter' => array( 'fields' => array('http_referer'), ), 'order_detail' => array( 'fields' => array('group_reduction', 'ecotax_tax_rate', 'id_currency', 'reduction_percent', 'reduction_amount'), ), 'webservice_account' => array( 'fields' => array('is_module', 'module_name'), ), ); /** * @var string Path to install directory */ private $installPath; /** * @var array Store schemas of all upgrades */ private $schemas = array(); /** * @var array Store db.sql schema */ private $generalSchema = null; /** * @var array Store current database schema */ private $currentSchema = null; /** * @var bool If set to false, will compare all data of each table to get full error log */ private $fastExecution = false; /** * @var bool If true, fields type will be compared too */ private $compareTypes = true; /** * @var int Store microtime of the script execution */ private $totalTime = 0; /** * @var array Error log */ private $errors = array(); /** * @var array Prefix for current database */ private $prefix; /** * @var string Found versions */ private $versions = array(); /** * Constructor, launch the compare algorithm * * @param bool $compareTypes If true, will compare field types * @param bool $fastExecution If false, will loose some performance but log all errors */ public function __construct($compareTypes = true, $fastExecution = false) { // added for compatibility prestashop version < 1.3.7 if (!defined('_PS_CACHE_ENABLED_')) define('_PS_CACHE_ENABLED_', '0'); $this->installPath = INSTALL_PATH . '/'; $this->prefix = _DB_PREFIX_; $this->compareTypes = $compareTypes; $this->fastExecution = $fastExecution; $start = microtime(true); $this->generateSchemasFromUpdates(); $this->getDatabaseSchema(); $this->versions = $this->searchCurrentVersion(); $this->totalTime = microtime(true) - $start; } /** * @return int Get total execution time of class */ public function getTotalTime() { return $this->totalTime; } /** * @return array Get list of differences between current database and all upgrades */ public function getErrors($version = null) { if (!is_null($version)) { return isset($this->errors[$version]) ? $this->errors[$version] : array(); } return $this->errors; } /** * @return string Get found versions */ public function getVersions() { return $this->versions; } /** * Add more informations in database schema property in order to fix some troubles with missing data between db.sql and upgrades */ private function completeDatabaseSchema() { // Add key "discount" in "discount_category" table, because the name is different between the 1.2.2 upgrade and db.sql if (isset($this->currentSchema['discount_category']['@keys']['id_discount'])) $this->currentSchema['discount_category']['@keys']['discount'] = $this->currentSchema['discount_category']['@keys']['id_discount']; } /** * Get the schema of all updates */ private function generateSchemasFromUpdates() { // Get list of sorted upgrades $fd = opendir($this->installPath . 'sql/upgrade'); while ($file = readdir($fd)) { if (substr($file, -3, 3) == 'sql' && version_compare(substr($file, 0, -4), INSTALL_VERSION) <= 0) { $list[] = $file; } } closedir($fd); usort($list, 'version_compare'); $list = array_reverse($list); for ($i = 0, $total = count($list); $i < $total - 1; $i++) { $file = $list[$i]; $next = $list[$i + 1]; $this->generateSchema(substr($file, 0, -4), substr($next, 0, -4)); } } /** * Generate the schema of the given version * * @param string $currentVersion Version name * @param string $nextVersion Next version */ private function generateSchema($currentVersion, $nextVersion) { static $storeTypes = array(); // Load db.sql once if (is_null($this->generalSchema)) { $this->loadGeneralSchema(); } $this->schemas[] = array( 'name' => $nextVersion, 'struct' => array(), ); // Read queries in upgrade files, and reverse structure from db.sql if (!file_exists($this->installPath . 'sql/upgrade/' . $currentVersion . '.sql')) { throw new Exception('File sql/upgrade/' . $currentVersion . '.sql not found'); } $struct = (count($this->schemas) >= 2) ? $this->schemas[count($this->schemas) - 2]['struct'] : $this->generalSchema; $content = file_get_contents($this->installPath . 'sql/upgrade/' . $currentVersion . '.sql'); $queries = array_reverse(preg_split("/;\s*[\r\n]+/", $content)); foreach ($queries as $query) { $query = trim($query); // ALTER TABLE -> add, delete or update fields if (preg_match('#^alter\s+table\s+`?prefix_([a-z0-9_]+)`?\s+#si', $query, $m)) { $table = $m[1]; // Alter drop index preg_match_all('#\s*drop\s+(index|primary key)(\s+`?([a-z0-9_]+)`?)?#si', $query, $m); for ($i = 0, $total = count($m[0]); $i < $total; $i++) { $name = $m[3][$i]; if (strtolower($m[1][$i]) == 'primary key') { $name = 'primary'; } $struct[$table]['@keys'][$name] = '?'; $query = str_replace($m[0][$i], '', $query); } // Alter add index preg_match_all('#\s*add\s+(index|primary key|key|unique key|unique)\s*\(?([a-z0-9_,`\s]+)\)?(\s+\(([a-z0-9_,` ]+)\))?\s*(,|;|$)#si', $query, $m); for ($i = 0, $total = count($m[0]); $i < $total; $i++) { $name = str_replace(array('`', ' '), array('', ''), $m[2][$i]); $tmpName = explode(',', $name); $name = $tmpName[0]; if (strtolower($m[1][$i] == 'primary key')) { $name = 'primary'; } if (isset($struct[$table]['@keys']) && !isset($struct[$table]['@keys'][$name])) { // If key name doesn't exist, we try to match with list of fields and type $type = strtolower($m[1][$i]); if ($type == 'primary key') { $type = 'primary'; } elseif ($type == 'key') { $type = 'index'; } elseif ($type == 'unique key') { $type = 'unique'; } $fields = ($m[4][$i]) ? $m[4][$i] : $m[2][$i]; $fields = explode(',', preg_replace('#[\s`]#si', '', $fields)); foreach ($struct[$table]['@keys'] as $key => $data) { if ($data['type'] == $type && !array_diff($data['fields'], $fields)) { // Found it ! unset($struct[$table]['@keys'][$key]); break; } } } else { unset($struct[$table]['@keys'][$name]); } $query = str_replace($m[0][$i], '', $query); } // Alter add preg_match_all('#\s*add\s+`?([a-z0-9_]+)`?\s+(([a-z]+)(\([ 0-9,]+\))?(\s+unsigned)?)(\s+not)?(\s+null)?(\s+auto_increment)?(\s+primary)?#si', $query, $m); for ($i = 0, $total = count($m[0]); $i < $total; $i++) { unset($struct[$table][$m[1][$i]]); if (strtolower(trim($m[9][$i])) == 'primary') { unset($struct[$table]['@keys']['primary']); } } // Alter drop preg_match_all('#\s*drop\s+`?([a-z0-9_]+)`?#si', $query, $m); for ($i = 0, $total = count($m[0]); $i < $total; $i++) { $struct[$table][$m[1][$i]] = '?'; } // Alter change preg_match_all('#\s*change\s+`([a-z0-9_]+)`\s+`?([a-z0-9_]+)`?\s+([a-z]+)(\s*\([ 0-9,]+\))?(\s+unsigned)?#si', $query, $m); for ($i = 0, $total = count($m[0]); $i < $total; $i++) { if ($m[1][$i] != $m[2][$i]) { unset($struct[$table][$m[1][$i]]); } $struct[$table][$m[2][$i]] = '?'; } } // CREATE TABLE -> delete this table from structure elseif (preg_match('#^create\s+table\s+(if\s+not\s+exists\s+)?`?prefix_([a-z0-9_]+)`?#si', $query, $m) && !preg_match('#_tmp[0-9]?$#i', $m[2])) { unset($struct[$m[2]]); } // DROP TABLE -> add this table in structure elseif (preg_match('#^drop\s*table\s+(if\s+exists\s+)?`?prefix_([a-z0-9_]+)`?#si', $query, $m) && !preg_match('#_tmp[0-9]?$#i', $m[2])) { $struct[$m[2]] = array(); } } $this->schemas[count($this->schemas) - 1]['struct'] = $struct; } /** * Read db.sql file and load tables structure */ private function loadGeneralSchema() { $this->generalSchema = array(); if (!file_exists($this->installPath . 'sql/db.sql')) { throw new Exception('File sql/db.sql not found'); } // Get create queries from db.sql file $content = file_get_contents($this->installPath . 'sql/db.sql'); $queries = preg_split("/;\s*[\r\n]+/", $content); foreach ($queries as $query) { $query = trim($query); if (!preg_match('#^create table `?prefix_([a-z0-9_]+)`?\s#i', $query, $m)) { continue; } $table = $m[1]; $this->generalSchema[$table] = array(); // Get fields list preg_match_all('#(\(|,)\s*`?([a-z0-9_]+)`?\s+(([a-z]+)(\([ 0-9,]+\))?(\s+unsigned)?)\s*[^,]*#si', $query, $m); for ($i = 0, $total = count($m[0]); $i < $total; $i++) { if (in_array(strtolower($m[2][$i]), array('primary', 'key', 'unique', 'index'))) { continue; } $this->generalSchema[$table][$m[2][$i]] = $this->parseType($m[4][$i]); } // Get index $this->generalSchema[$table]['@keys'] = array(); preg_match_all('#(primary\s+key|unique\s+key|key|index)\s+\(?`?([a-z0-9_,` ]+)`?\)?(\s+\(([a-z0-9_,` ]+)\))?\s*(,|\))#si', $query, $m); for ($i = 0, $total = count($m[0]); $i < $total; $i++) { $type = strtolower($m[1][$i]); if ($type == 'primary key') { $type = 'primary'; } elseif ($type == 'key') { $type = 'index'; } elseif ($type == 'unique key') { $type = 'unique'; } $name = str_replace(array('`', ' '), array('', ''), $m[2][$i]); $tmpName = explode(',', $name); $name = $tmpName[0]; if ($type == 'primary') { $name = 'primary'; } $fields = ($m[4][$i]) ? $m[4][$i] : $m[2][$i]; $fields = explode(',', preg_replace('#[\s`]#si', '', $fields)); $this->generalSchema[$table]['@keys'][$name] = array( 'type' => $type, 'fields' => $fields, ); } } $this->schemas[] = array( 'name' => INSTALL_VERSION, 'struct' => $this->generalSchema, ); } /** * Get the schema of current database */ private function getDatabaseSchema() { $struct = array(); $sql = 'SHOW TABLES'; foreach (Db::getInstance()->executeS($sql) as $row) { $table = current($row); if (substr($table, 0, strlen($this->prefix)) != $this->prefix) { continue; } $virtualTable = substr($table, strlen($this->prefix)); $struct[$virtualTable] = array(); // List fields $sql = 'SHOW FIELDS FROM ' . $table; foreach (Db::getInstance()->executeS($sql) as $rowField) { $struct[$virtualTable][$rowField['Field']] = $this->parseType($rowField['Type']); } // List keys $struct[$virtualTable]['@keys'] = array(); $sql = 'SHOW INDEX FROM ' . $table; $results = Db::getInstance()->executeS($sql); if($results) foreach ($results as $rowIndex) { $keyName = strtolower($rowIndex['Key_name']); $type = 'index'; if ($keyName == 'primary') { $type = 'primary'; } elseif (!$rowIndex['Non_unique']) { $type = 'unique'; } if (!isset($struct[$virtualTable]['@keys'][$keyName])) { $struct[$virtualTable]['@keys'][$keyName] = array( 'type' => $type, 'fields' => array(), ); } $struct[$virtualTable]['@keys'][$keyName]['fields'][] = $rowIndex['Column_name']; } } $this->currentSchema = $struct; $this->completeDatabaseSchema(); } /** * Launch comparison and search current version */ private function searchCurrentVersion() { $this->errors = array(); $version = null; $versions = array(); $state = 0; // Browse all schema, we will compare all data of listed schema to current database structure, evertytime something is different // we break and go to next schema foreach ($this->schemas as $schema) { $struct = $schema['struct']; $this->errors[$schema['name']] = array(); $isThisVersion = true; // Browse all table for schema foreach ($struct as $table => $fields) { // Is this table ignored ? if (isset($this->ignore[$table]) && !$this->ignore[$table]) { continue; } // Check if table exists if (!isset($this->currentSchema[$table])) { $this->errors[$schema['name']][] = "Table '$table' not found"; $isThisVersion = false; if ($this->fastExecution) { break; } continue; } // Browse all fields for this table foreach ($fields as $field => $type) { if ($field == '@keys') { continue; } // Is this field ignored ? if (isset($this->ignore[$table], $this->ignore[$table]['fields']) && in_array($field, $this->ignore[$table]['fields'])) { continue; } // Check if field exists if (!isset($this->currentSchema[$table][$field])) { $this->errors[$schema['name']][] = "Field '$field' in table '$table' not found"; $isThisVersion = false; if ($this->fastExecution) { break 2; } continue; } // Is this type ignored ? if (isset($this->ignore[$table], $this->ignore[$table]['types']) && in_array($field, $this->ignore[$table]['types'])) { continue; } // Compare the field type, ignore comparaison if we don't know the field type (? char) if ($this->compareTypes && $type != '?' && $this->currentSchema[$table][$field] != $type) { $this->errors[$schema['name']][] = "The field '$field' in table '$table' has a different type ('$type' :: '{$this->currentSchema[$table][$field]}')"; $isThisVersion = false; if ($this->fastExecution) { break 2; } } } // Compare index if (isset($fields['@keys'])) { foreach ($fields['@keys'] as $name => $data) { if ($data == '?') { continue; } // Is this index ignored ? if (isset($this->ignore[$table], $this->ignore[$table]['keys']) && in_array($name, $this->ignore[$table]['keys'])) { continue; } // Test if index exists if (!isset($this->currentSchema[$table]['@keys'][$name])) { $this->errors[$schema['name']][] = "Key '$name' in table '$table' not found"; $isThisVersion = false; if ($this->fastExecution) { break 2; } continue; } // Compare index type if ($this->currentSchema[$table]['@keys'][$name]['type'] != $data['type']) { $this->errors[$schema['name']][] = "Key '$name' in table '$table' has different type ('{$this->currentSchema[$table]['@keys'][$name]['type']}', '{$data['type']}')"; $isThisVersion = false; if ($this->fastExecution) { break 2; } } // Compare list of fields $diff = array_diff($this->currentSchema[$table]['@keys'][$name]['fields'], $data['fields']); if ($diff) { $this->errors[$schema['name']][] = "Key '$name' in table '$table' has different fields ('" . implode("', '", $diff) . "')"; $isThisVersion = false; if ($this->fastExecution) { break 2; } } } } } if ($isThisVersion) { $versions[] = $schema['name']; $state = 1; } if (!$isThisVersion && $state == 1) { break; } } return ($versions); } /** * Format correctly a field type name * * @param string $type Type name * @return Parsed type name */ private function parseType($type) { $type = strtolower(preg_replace('#^([a-z]+).*$#i', '\\1', $type)); $type = str_replace('boolean', 'tinyint', $type); return $type; } }