options['current_db_version']; if ( ( 0 != $current ) && ( $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 ( 0 != $current ) { 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 ); //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared if ( false === $rez ) { $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 ); //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $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 ); //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared } } 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 ); $tables = $wpdb->get_col('SHOW TABLES;'); if ( $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 = array(); $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};" );//phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $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'] && ( 'NO' == $tablefield->Null ) ) || ( $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}`;" ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $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 ( 'primary' == $keyname ) { $index_ary[ $keyname ]['index_modifier'] = 'primary'; } elseif ( 0 == $tableindex->Non_unique ) { $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 ( 'primary' == $index_name ) { $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'] = ( false !== $wpdb->query( $query ) );//phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $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 ( 'primary' == $index_modifier ) { $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 = null; $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'] ) || ( 'primary' != $definition['index_modifier'] ) ) { $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; } }