options['current_db_version'];
if ( ($current != 0) && ( $current < 4 ) ){
//The 4th DB version makes a lot of backwards-incompatible changes to the main
//BLC tables, so instead of upgrading we just throw them away and recreate.
if ( !blcDatabaseUpgrader::drop_tables() ){
return false;
};
$current = 0;
}
//Create/update the plugin's tables
if ( !blcDatabaseUpgrader::make_schema_current() ) {
return false;
}
if ( $current != 0 ){
if ( $current < 5 ){
blcDatabaseUpgrader::upgrade_095();
}
}
$conf->options['current_db_version'] = BLC_DATABASE_VERSION;
$conf->save_options();
$blclog->info('Database successfully upgraded.');
return true;
}
/**
* Create or update the plugin's DB tables.
*
* @return bool
*/
static function make_schema_current(){
global $blclog;
$start = microtime(true);
if ( !function_exists('blc_get_db_schema') ){
require 'db-schema.php';
}
list($dummy, $query_log) = blcTableDelta::delta(blc_get_db_schema());
$have_errors = false;
foreach($query_log as $item){
if ( $item['success'] ){
$blclog->info(' [OK] ' . $item['query'] . sprintf(' (%.3f seconds)', $item['query_time']));
} else {
$blclog->error(' [ ] ' . $item['query']);
$blclog->error(' Database error : ' . $item['error_message']);
$have_errors = true;
}
}
$blclog->info(sprintf('Schema update took %.3f seconds', microtime(true) - $start));
$blclog->info('Database schema updated.');
return !$have_errors;
}
/**
* Drop the plugin's tables.
*
* @return bool
*/
static function drop_tables(){
global $wpdb, $blclog; /** @var wpdb $wpdb */
$blclog->info('Deleting the plugin\'s database tables');
$tables = array(
$wpdb->prefix . 'blc_linkdata',
$wpdb->prefix . 'blc_postdata',
$wpdb->prefix . 'blc_instances',
$wpdb->prefix . 'blc_synch',
$wpdb->prefix . 'blc_links',
);
$q = "DROP TABLE IF EXISTS " . implode(', ', $tables);
$rez = $wpdb->query( $q );
if ( $rez === false ){
$error = sprintf(
__("Failed to delete old DB tables. Database error : %s", 'broken-link-checker'),
$wpdb->last_error
);
$blclog->error($error);
/*
//FIXME: In very rare cases, DROP TABLE IF EXISTS throws an error when the table(s) don't exist.
return false;
//*/
}
$blclog->info('Done.');
return true;
}
static function upgrade_095($trigger_errors = false){
global $wpdb; /** @var wpdb $wpdb */
//Prior to 0.9.5 all supported post types were internally represented using
//a common 'post' container type. The current version creates a unique container
//type to each post type.
//Update synch records and instances to reflect this change
$q = "
UPDATE
{$wpdb->prefix}blc_synch AS synch
LEFT JOIN {$wpdb->posts} AS posts ON (posts.ID = synch.container_id)
SET
synch.container_type = posts.post_type
WHERE
synch.container_type = 'post' AND posts.post_type IS NOT NULL";
$wpdb->query($q);
$q = "
UPDATE
{$wpdb->prefix}blc_instances AS instances
LEFT JOIN {$wpdb->posts} AS posts ON (posts.ID = instances.container_id)
SET
instances.container_type = posts.post_type
WHERE
instances.container_type = 'post' AND posts.post_type IS NOT NULL";
$wpdb->query($q);
}
}
class blcTableDelta {
/**
* Parse one or more CREATE TABLE queries and generate a list of SQL queries that need
* to be executed to make the current database schema match those queries. Will also
* execute those queries by default.
*
* This function returns an array with two items. The first is a list of human-readable
* messages explaining what database changes were/would be made. The second array item
* is an array of the generated SQL queries and (if $execute was True) their results.
*
* Each item of this second array is itself an associative array with these keys :
* 'query' - the generated query.
* 'success' - True if the query was executed successfully, False if it caused an error.
* 'error_message' - the MySQL error message (only meaningful when 'success' = false).
*
* The 'success' and 'error_message' keys will only be present if $execute was set to True.
*
* @param string $queries One or more CREATE TABLE queries separated by a semicolon.
* @param bool $execute Whether to apply the schema changes. Defaults to true.
* @param bool $drop_columns Whether to drop columns not present in the input. Defaults to true.
* @param bool $drop_indexes Whether to drop indexes not present in the input. Defaults to true.
* @return array
*/
static function delta($queries, $execute = true, $drop_columns = true, $drop_indexes = true){
global $wpdb, $blclog; /** @var wpdb $wpdb */
// Separate individual queries into an array
if ( !is_array($queries) ) {
$queries = explode( ';', $queries );
if ('' == $queries[count($queries) - 1]) array_pop($queries);
}
$cqueries = array(); // Creation Queries
$for_update = array();
// Create a tablename index for an array ($cqueries) of queries
foreach($queries as $qry) {
if (preg_match("|CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?([^\s(]+)|i", $qry, $matches)) {
$table = trim( $matches[1], '`' );
$cqueries[$table] = $qry;
$for_update[$table] = 'Create table `'.$table.'`';
}
}
// Check to see which tables and fields exist
$start_show_tables = microtime(true);
if ($tables = $wpdb->get_col('SHOW TABLES;')) {
$blclog->info(sprintf('... SHOW TABLES (%.3f seconds)', microtime(true) - $start_show_tables));
// For every table in the database
foreach ($tables as $table) {
// If a table query exists for the database table...
if ( array_key_exists($table, $cqueries) ) {
// Clear the field and index arrays
$cfields = $indices = array();
// Get all of the field names in the query from between the parens
preg_match("|\((.*)\)|ms", $cqueries[$table], $match2);
$qryline = trim($match2[1]);
// Separate field lines into an array
$flds = preg_split('@[\r\n]+@', $qryline);
//echo "
\n".print_r(strtolower($table), true).":\n".print_r($flds, true)."
";
// For every field line specified in the query
foreach ($flds as $fld) {
$definition = blcTableDelta::parse_create_definition($fld);
if ( $definition ){
if ( $definition['index'] ){
$indices[ $definition['index_definition'] ] = $definition; //Index
} else {
$cfields[ $definition['name'] ] = $definition; //Column
}
}
}
//echo "Detected fields :
"; print_r($cfields);
// Fetch the table column structure from the database
$start = microtime(true);
$tablefields = $wpdb->get_results("SHOW FULL COLUMNS FROM {$table};");
$blclog->info(sprintf('... SHOW FULL COLUMNS FROM %s %.3f seconds', $table, microtime(true) - $start));
// For every field in the table
foreach ($tablefields as $tablefield) {
$field_name = strtolower($tablefield->Field); //Field names are case-insensitive in MySQL
// If the table field exists in the field array...
if (array_key_exists($field_name, $cfields)) {
$definition = $cfields[$field_name];
// Is actual field definition different from that in the query?
$different =
( $tablefield->Type != $definition['data_type'] ) ||
( $definition['collation'] && ($tablefield->Collation != $definition['collation']) ) ||
( $definition['null_allowed'] && ($tablefield->Null == 'NO') ) ||
( !$definition['null_allowed'] && ($tablefield->Null == 'YES') ) ||
( $tablefield->Default !== $definition['default'] );
// Add a query to change the column type
if ( $different ) {
$cqueries[] = "ALTER TABLE `{$table}` MODIFY COLUMN `{$field_name}` {$definition['column_definition']}";
$for_update[$table.'.'.$field_name] = "Changed type of {$table}.{$field_name} from {$tablefield->Type} to {$definition['column_definition']}";
}
// Remove the field from the array (so it's not added)
unset($cfields[$field_name]);
} else {
// This field exists in the table, but not in the creation queries? Drop it.
if ( $drop_columns ){
$cqueries[] = "ALTER TABLE `{$table}` DROP COLUMN `$field_name`";
$for_update[$table.'.'.$field_name] = 'Removed column '.$table.'.'.$field_name;
}
}
}
// For every remaining field specified for the table
foreach ($cfields as $field_name => $definition) {
// Push a query line into $cqueries that adds the field to that table
$cqueries[] = "ALTER TABLE `{$table}` ADD COLUMN `$field_name` {$definition['column_definition']}";
$for_update[$table.'.'.$field_name] = 'Added column '.$table.'.'.$field_name;
}
// Index stuff goes here
//echo 'Detected indexes :
'; print_r($indices);
// Fetch the table index structure from the database
$start = microtime(true);
$tableindices = $wpdb->get_results("SHOW INDEX FROM `{$table}`;");
$blclog->info(sprintf('... SHOW INDEX FROM %s %.3f seconds', $table, microtime(true) - $start));
if ($tableindices) {
// Clear the index array
$index_ary = array();
// For every index in the table
foreach ($tableindices as $tableindex) {
// Add the index to the index data array
$keyname = strtolower($tableindex->Key_name);
$index_ary[$keyname]['name'] = $keyname;
$index_ary[$keyname]['columns'][] = array(
'column_name' => strtolower($tableindex->Column_name),
'length' => $tableindex->Sub_part
);
if ( !isset($index_ary[$keyname]['index_modifier']) ){
if ( $keyname == 'primary' ){
$index_ary[$keyname]['index_modifier'] = 'primary';
} else if ( $tableindex->Non_unique == 0 ){
$index_ary[$keyname]['index_modifier'] = 'unique';
}
}
}
// For each actual index in the index array
foreach ($index_ary as $index_name => $index_data) {
// Build a create string to compare to the query
$index_string = blcTableDelta::generate_index_string($index_data);
if ( array_key_exists($index_string, $indices) ){
//echo "Found index $index_string
";
unset($indices[$index_string]);
} else {
//echo "Didn't find index $index_string
";
if ( $drop_indexes ){
if ( $index_name == 'primary' ){
$cqueries[] = "ALTER TABLE `{$table}` DROP PRIMARY KEY";
} else {
$cqueries[] = "ALTER TABLE `{$table}` DROP KEY `$index_name`";
}
$for_update[$table.'.'.$index_name] = 'Removed index '.$table.'.'.$index_name;
}
}
}
}
// For every remaining index specified for the table
foreach ( $indices as $index ) {
// Push a query line into $cqueries that adds the index to that table
$cqueries[] = "ALTER TABLE `{$table}` ADD {$index['index_definition']}";
$for_update[$table.'.'.$index['name']] = 'Added index '.$table.' '.$index['index_definition'];
}
// Remove the original table creation query from processing
unset($cqueries[$table]);
unset($for_update[$table]);
} else {
// This table exists in the database, but not in the creation queries?
}
}
}
//echo "Execute queries :
"; print_r($cqueries);
$query_log = array();
foreach ($cqueries as $query) {
$log_item = array('query' => $query,);
if ( $execute ) {
$start = microtime(true);
$log_item['success'] = ($wpdb->query($query) !== false);
$log_item['error_message'] = $wpdb->last_error;
$log_item['query_time'] = microtime(true) - $start;
}
$query_log[] = $log_item;
}
return array($for_update, $query_log);
}
/**
* Parse a a single column or index definition.
*
* This function can parse many (but not all) types of syntax used to define columns
* and indexes in a "CREATE TABLE" query.
*
* @param string $line
* @return array
*/
static function parse_create_definition($line){
$line = preg_replace('@[,\r\n\s]+$@', '', $line); //Strip the ", " line separator
$pieces = preg_split('@\s+|(?=\()@', $line, -1, PREG_SPLIT_NO_EMPTY);
if ( empty($pieces) ){
return null;
}
$token = strtolower(array_shift($pieces));
$index_modifier = '';
$index = false;
//Determine if this line defines an index
if ( in_array($token, array('primary', 'unique', 'fulltext')) ){
$index_modifier = $token;
$index = true;
$token = strtolower(array_shift($pieces));
}
if ( in_array($token, array('index', 'key')) ){
$index = true;
$token = strtolower(array_shift($pieces));
}
//Determine column/index name
$name = '';
if ( $index ){
//Names are optional for indexes; the INDEX/etc keyword can be immediately
//followed by a column list (or index_type, but we're ignoring that possibility).
if ( strpos($token, '(') === false ){
$name = $token;
} else {
if ( $index_modifier == 'primary' ){
$name = 'primary';
}
array_unshift($pieces, $token);
}
} else {
$name = $token;
}
$name = strtolower(trim($name, '`'));
$definition = compact('name', 'index', 'index_modifier');
//Parse the rest of the line
$remainder = implode(' ', $pieces);
if ( $index ){
$definition['columns'] = blcTableDelta::parse_index_column_list($remainder);
//If the index doesn't have a name, use the name of the first column
//(this is what MySQL does, but only when there isn't already an index with that name).
if ( empty($definition['name']) ){
$definition['name'] = $definition['columns'][0]['column_name'];
}
//Rebuild the index def. in a normalized form
$definition['index_definition'] = blcTableDelta::generate_index_string($definition);
} else {
$column_def = blcTableDelta::parse_column_definition($remainder);
$definition = array_merge($definition, $column_def);
}
return $definition;
}
/**
* Parse the list of columns included in an index.
*
* This function returns a list of column descriptors. Each descriptor is
* an associative array with the keys 'column_name', 'length' and 'order'.
*
* @param string $line
* @return array Array of index columns
*/
static function parse_index_column_list($line){
$line = preg_replace('@^\s*\(|\)\s*$@', '', $line); //Strip the braces that surround the column list
$pieces = preg_split('@\s*,\s*@', $line);
$columns = array();
foreach($pieces as $piece){
if ( preg_match('@`?(?P[^\s`]+)`?(?:\s*\(\s*(?P\d+)\s*\))?(?:\s+(?PASC|DESC))?@i', $piece, $matches) ){
$column = array(
'column_name' => strtolower($matches['column_name']),
'length' => null,
'order' => null //unused; included for completeness
);
if ( isset($matches['length']) && is_numeric($matches['length']) ){
$column['length'] = intval($matches['length']);
}
if ( isset($matches['order']) && !empty($matches['order']) ){
$column['order'] = strtolower($matches['order']);
}
$columns[] = $column;
};
}
return $columns;
}
/**
* Parse column datatype and flags.
*
*
* @param string $line
* @return array
*/
static function parse_column_definition($line){
$line = trim($line);
//Extract datatype. This regexp is not entirely reliable - for example, it won't work
//with enum fields where one of values contains brackets "()".
$data_type = '';
$regexp = '
@
(?P^\w+)
# followed by an optional length or a list of enum values
(?:\s*
\(
\s* (?P[^()]+) \s*
\)
)?
# various type modifiers/keywords
(?P
(?:\s+
(?: BINARY | UNSIGNED | ZEROFILL )
)*
)?
@xi';
if ( preg_match($regexp, $line, $matches) ){
$data_type = strtolower($matches['type_name']);
if ( !empty($matches['length']) ){
$data_type .= '(' . trim($matches['length']) . ')';
}
if ( !empty($matches['keywords']) ){
$data_type .= preg_replace('@\s+@', ' ', $matches['keywords']); //Collapse spaces
}
$line = substr($line, strlen($data_type));
}
//Extract flags
$null_allowed = !preg_match('@\sNOT\s+NULL\b@i', $line);
$auto_increment = preg_match('@\sAUTO_INCREMENT\b@i', $line);
//Got a default value?
$default = null;
if ( preg_match("@\sDEFAULT\s+('[^']*'|\"[^\"]*\"|\d+)@i", $line, $matches) ){
$default = trim($matches[1], '"\'');
}
//Custom character set and/or collation?
$charset = $collation = null;
if ( preg_match('@ (?:\s CHARACTER \s+ SET \s+ (?P[^\s()]+) )? (?:\s COLLATE \s+ (?P[^\s()]+) )? @xi', $line, $matches) ){
if ( isset($matches['charset']) ){
$charset = $matches['charset'];
}
if ( isset($matches['collation']) ){
$collation = $matches['collation'];
}
}
//Generate the normalized column definition
$column_definition = $data_type;
if ( !empty($charset) ){
$column_definition .= " CHARACTER SET {$charset}";
}
if ( !empty($collation) ){
$column_definition .= " COLLATE {$collation}";
}
if ( !$null_allowed ){
$column_definition .= " NOT NULL";
}
if ( !is_null($default) ){
$column_definition .= " DEFAULT '{$default}'";
}
if ( $auto_increment ){
$column_definition .= " AUTO_INCREMENT";
}
return compact('data_type', 'null_allowed', 'auto_increment', 'default', 'charset', 'collation', 'column_definition');
}
/**
* Generate an index's definition string from its parsed representation.
*
* @param array $definition The return value of blcTableDelta::parse_create_definition()
* @return string
*/
static function generate_index_string($definition){
//Rebuild the index def. in a normalized form
$index_definition = '';
if ( !empty($definition['index_modifier']) ){
$index_definition .= strtoupper($definition['index_modifier']) . ' ';
}
$index_definition .= 'KEY';
if ( empty($definition['index_modifier']) || ($definition['index_modifier'] != 'primary') ){
$index_definition .= ' `' . $definition['name'].'`';
}
$column_strings = array();
foreach($definition['columns'] as $column){
$c = '`' . $column['column_name'] . '`';
if ( $column['length'] ){
$c .= '('.$column['length'].')';
}
$column_strings[] = $c;
}
$index_definition .= ' (' . implode(', ', $column_strings) . ')';
return $index_definition;
}
}