PageRenderTime 51ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/wp-content/plugins/broken-link-checker/includes/admin/db-upgrade.php

https://bitbucket.org/lgorence/quickpress
PHP | 577 lines | 370 code | 86 blank | 121 comment | 73 complexity | e0b1ea23ecfdb35ac04f2f15e0c409b9 MD5 | raw file
Possible License(s): GPL-2.0, LGPL-2.1, AGPL-1.0
  1. <?php
  2. class blcDatabaseUpgrader {
  3. /**
  4. * Create and/or upgrade the plugin's database tables.
  5. *
  6. * @return bool
  7. */
  8. public static function upgrade_database(){
  9. global $wpdb, $blclog;
  10. $conf = blc_get_configuration();
  11. $current = $conf->options['current_db_version'];
  12. if ( ($current != 0) && ( $current < 4 ) ){
  13. //The 4th DB version makes a lot of backwards-incompatible changes to the main
  14. //BLC tables, so instead of upgrading we just throw them away and recreate.
  15. if ( !blcDatabaseUpgrader::drop_tables() ){
  16. return false;
  17. };
  18. $current = 0;
  19. }
  20. //Create/update the plugin's tables
  21. if ( !blcDatabaseUpgrader::make_schema_current() ) {
  22. return false;
  23. }
  24. if ( $current != 0 ){
  25. if ( $current < 5 ){
  26. blcDatabaseUpgrader::upgrade_095();
  27. }
  28. }
  29. $conf->options['current_db_version'] = BLC_DATABASE_VERSION;
  30. $conf->save_options();
  31. $blclog->info('Database successfully upgraded.');
  32. return true;
  33. }
  34. /**
  35. * Create or update the plugin's DB tables.
  36. *
  37. * @return bool
  38. */
  39. function make_schema_current(){
  40. global $blclog;
  41. if ( !function_exists('blc_get_db_schema') ){
  42. require 'db-schema.php';
  43. }
  44. list($dummy, $query_log) = blcTableDelta::delta(blc_get_db_schema());
  45. $have_errors = false;
  46. foreach($query_log as $item){
  47. if ( $item['success'] ){
  48. $blclog->info(' [OK] ' . $item['query']);
  49. } else {
  50. $blclog->error(' [ ] ' . $item['query']);
  51. $blclog->error(' Database error : ' . $item['error_message']);
  52. $have_errors = true;
  53. }
  54. }
  55. $blclog->info('Database schema updated.');
  56. return !$have_errors;
  57. }
  58. /**
  59. * Drop the plugin's tables.
  60. *
  61. * @return bool
  62. */
  63. function drop_tables(){
  64. global $wpdb, $blclog;
  65. $blclog->info('Deleting the plugin\'s database tables');
  66. $tables = array(
  67. $wpdb->prefix . 'blc_linkdata',
  68. $wpdb->prefix . 'blc_postdata',
  69. $wpdb->prefix . 'blc_instances',
  70. $wpdb->prefix . 'blc_synch',
  71. $wpdb->prefix . 'blc_links',
  72. );
  73. $q = "DROP TABLE IF EXISTS " . implode(', ', $tables);
  74. $rez = $wpdb->query( $q );
  75. if ( $rez === false ){
  76. $error = sprintf(
  77. __("Failed to delete old DB tables. Database error : %s", 'broken-link-checker'),
  78. $wpdb->last_error
  79. );
  80. $blclog->error($error);
  81. /*
  82. //FIXME: In very rare cases, DROP TABLE IF EXISTS throws an error when the table(s) don't exist.
  83. return false;
  84. //*/
  85. }
  86. $blclog->info('Done.');
  87. return true;
  88. }
  89. function upgrade_095($trigger_errors = false){
  90. global $wpdb; /** @var wpdb $wpdb */
  91. //Prior to 0.9.5 all supported post types were internally represented using
  92. //a common 'post' container type. The current version creates a unique container
  93. //type to each post type.
  94. //Update synch records and instances to reflect this change
  95. $q = "
  96. UPDATE
  97. {$wpdb->prefix}blc_synch AS synch
  98. LEFT JOIN {$wpdb->posts} AS posts ON (posts.ID = synch.container_id)
  99. SET
  100. synch.container_type = posts.post_type
  101. WHERE
  102. synch.container_type = 'post' AND posts.post_type IS NOT NULL";
  103. $wpdb->query($q);
  104. $q = "
  105. UPDATE
  106. {$wpdb->prefix}blc_instances AS instances
  107. LEFT JOIN {$wpdb->posts} AS posts ON (posts.ID = instances.container_id)
  108. SET
  109. instances.container_type = posts.post_type
  110. WHERE
  111. instances.container_type = 'post' AND posts.post_type IS NOT NULL";
  112. $wpdb->query($q);
  113. }
  114. }
  115. class blcTableDelta {
  116. /**
  117. * Parse one or more CREATE TABLE queries and generate a list of SQL queries that need
  118. * to be executed to make the current database schema match those queries. Will also
  119. * execute those queries by default.
  120. *
  121. * This function returns an array with two items. The first is a list of human-readable
  122. * messages explaining what database changes were/would be made. The second array item
  123. * is an array of the generated SQL queries and (if $execute was True) their results.
  124. *
  125. * Each item of this second array is itself an associative array with these keys :
  126. * 'query' - the generated query.
  127. * 'success' - True if the query was executed successfully, False if it caused an error.
  128. * 'error_message' - the MySQL error message (only meaningful when 'success' = false).
  129. *
  130. * The 'success' and 'error_message' keys will only be present if $execute was set to True.
  131. *
  132. * @param string $queries One or more CREATE TABLE queries separated by a semicolon.
  133. * @param bool $execute Whether to apply the schema changes. Defaults to true.
  134. * @param bool $drop_columns Whether to drop columns not present in the input. Defaults to true.
  135. * @param bool $drop_indexes Whether to drop indexes not present in the input. Defaults to true.
  136. * @return array
  137. */
  138. static function delta($queries, $execute = true, $drop_columns = true, $drop_indexes = true){
  139. global $wpdb;
  140. // Separate individual queries into an array
  141. if ( !is_array($queries) ) {
  142. $queries = explode( ';', $queries );
  143. if ('' == $queries[count($queries) - 1]) array_pop($queries);
  144. }
  145. $cqueries = array(); // Creation Queries
  146. $for_update = array();
  147. // Create a tablename index for an array ($cqueries) of queries
  148. foreach($queries as $qry) {
  149. if (preg_match("|CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?([^\s(]+)|i", $qry, $matches)) {
  150. $table = trim( strtolower($matches[1]), '`' );
  151. $cqueries[$table] = $qry;
  152. $for_update[$table] = 'Create table `'.$table.'`';
  153. }
  154. }
  155. // Check to see which tables and fields exist
  156. if ($tables = $wpdb->get_col('SHOW TABLES;')) {
  157. // For every table in the database
  158. foreach ($tables as $table) {
  159. $table = strtolower($table);
  160. // If a table query exists for the database table...
  161. if ( array_key_exists($table, $cqueries) ) {
  162. // Clear the field and index arrays
  163. $cfields = $indices = array();
  164. // Get all of the field names in the query from between the parens
  165. preg_match("|\((.*)\)|ms", $cqueries[$table], $match2);
  166. $qryline = trim($match2[1]);
  167. // Separate field lines into an array
  168. $flds = preg_split('@[\r\n]+@', $qryline);
  169. //echo "<hr/><pre>\n".print_r(strtolower($table), true).":\n".print_r($flds, true)."</pre><hr/>";
  170. // For every field line specified in the query
  171. foreach ($flds as $fld) {
  172. $definition = blcTableDelta::parse_create_definition($fld);
  173. if ( $definition ){
  174. if ( $definition['index'] ){
  175. $indices[ $definition['index_definition'] ] = $definition; //Index
  176. } else {
  177. $cfields[ $definition['name'] ] = $definition; //Column
  178. }
  179. }
  180. }
  181. //echo "Detected fields : <br>"; print_r($cfields);
  182. // Fetch the table column structure from the database
  183. $tablefields = $wpdb->get_results("SHOW FULL COLUMNS FROM {$table};");
  184. // For every field in the table
  185. foreach ($tablefields as $tablefield) {
  186. $field_name = strtolower($tablefield->Field); //Field names are case-insensitive in MySQL
  187. // If the table field exists in the field array...
  188. if (array_key_exists($field_name, $cfields)) {
  189. $definition = $cfields[$field_name];
  190. // Is actual field definition different from that in the query?
  191. $different =
  192. ( $tablefield->Type != $definition['data_type'] ) ||
  193. ( $definition['collation'] && ($tablefield->Collation != $definition['collation']) ) ||
  194. ( $definition['null_allowed'] && ($tablefield->Null == 'NO') ) ||
  195. ( !$definition['null_allowed'] && ($tablefield->Null == 'YES') ) ||
  196. ( $tablefield->Default !== $definition['default'] );
  197. // Add a query to change the column type
  198. if ( $different ) {
  199. $cqueries[] = "ALTER TABLE `{$table}` MODIFY COLUMN `{$field_name}` {$definition['column_definition']}";
  200. $for_update[$table.'.'.$field_name] = "Changed type of {$table}.{$field_name} from {$tablefield->Type} to {$definition['column_definition']}";
  201. }
  202. // Remove the field from the array (so it's not added)
  203. unset($cfields[$field_name]);
  204. } else {
  205. // This field exists in the table, but not in the creation queries? Drop it.
  206. if ( $drop_columns ){
  207. $cqueries[] = "ALTER TABLE `{$table}` DROP COLUMN `$field_name`";
  208. $for_update[$table.'.'.$field_name] = 'Removed column '.$table.'.'.$field_name;
  209. }
  210. }
  211. }
  212. // For every remaining field specified for the table
  213. foreach ($cfields as $field_name => $definition) {
  214. // Push a query line into $cqueries that adds the field to that table
  215. $cqueries[] = "ALTER TABLE `{$table}` ADD COLUMN `$field_name` {$definition['column_definition']}";
  216. $for_update[$table.'.'.$field_name] = 'Added column '.$table.'.'.$field_name;
  217. }
  218. // Index stuff goes here
  219. //echo 'Detected indexes : <br>'; print_r($indices);
  220. // Fetch the table index structure from the database
  221. $tableindices = $wpdb->get_results("SHOW INDEX FROM `{$table}`;");
  222. if ($tableindices) {
  223. // Clear the index array
  224. unset($index_ary);
  225. // For every index in the table
  226. foreach ($tableindices as $tableindex) {
  227. // Add the index to the index data array
  228. $keyname = strtolower($tableindex->Key_name);
  229. $index_ary[$keyname]['name'] = $keyname;
  230. $index_ary[$keyname]['columns'][] = array(
  231. 'column_name' => strtolower($tableindex->Column_name),
  232. 'length' => $tableindex->Sub_part
  233. );
  234. if ( !isset($index_ary[$keyname]['index_modifier']) ){
  235. if ( $keyname == 'primary' ){
  236. $index_ary[$keyname]['index_modifier'] = 'primary';
  237. } else if ( $tableindex->Non_unique == 0 ){
  238. $index_ary[$keyname]['index_modifier'] = 'unique';
  239. }
  240. }
  241. }
  242. // For each actual index in the index array
  243. foreach ($index_ary as $index_name => $index_data) {
  244. // Build a create string to compare to the query
  245. $index_string = blcTableDelta::generate_index_string($index_data);
  246. if ( array_key_exists($index_string, $indices) ){
  247. //echo "Found index $index_string<br>";
  248. unset($indices[$index_string]);
  249. } else {
  250. //echo "Didn't find index $index_string<br>";
  251. if ( $drop_indexes ){
  252. if ( $index_name == 'primary' ){
  253. $cqueries[] = "ALTER TABLE `{$table}` DROP PRIMARY KEY";
  254. } else {
  255. $cqueries[] = "ALTER TABLE `{$table}` DROP KEY `$index_name`";
  256. }
  257. $for_update[$table.'.'.$index_name] = 'Removed index '.$table.'.'.$index_name;
  258. }
  259. }
  260. }
  261. }
  262. // For every remaining index specified for the table
  263. foreach ( $indices as $index ) {
  264. // Push a query line into $cqueries that adds the index to that table
  265. $cqueries[] = "ALTER TABLE `{$table}` ADD {$index['index_definition']}";
  266. $for_update[$table.'.'.$index['name']] = 'Added index '.$table.' '.$index['index_definition'];
  267. }
  268. // Remove the original table creation query from processing
  269. unset($cqueries[strtolower($table)]);
  270. unset($for_update[strtolower($table)]);
  271. } else {
  272. // This table exists in the database, but not in the creation queries?
  273. }
  274. }
  275. }
  276. //echo "Execute queries : <br>"; print_r($cqueries);
  277. $query_log = array();
  278. foreach ($cqueries as $query) {
  279. $log_item = array('query' => $query,);
  280. if ( $execute ) {
  281. $log_item['success'] = ($wpdb->query($query) !== false);
  282. $log_item['error_message'] = $wpdb->last_error;
  283. }
  284. $query_log[] = $log_item;
  285. }
  286. return array($for_update, $query_log);
  287. }
  288. /**
  289. * Parse a a single column or index definition.
  290. *
  291. * This function can parse many (but not all) types of syntax used to define columns
  292. * and indexes in a "CREATE TABLE" query.
  293. *
  294. * @param string $line
  295. * @return array
  296. */
  297. function parse_create_definition($line){
  298. $line = preg_replace('@[,\r\n\s]+$@', '', $line); //Strip the ", " line separator
  299. $pieces = preg_split('@\s+|(?=\()@', $line, -1, PREG_SPLIT_NO_EMPTY);
  300. if ( empty($pieces) ){
  301. return null;
  302. }
  303. $token = strtolower(array_shift($pieces));
  304. $index_modifier = '';
  305. $index = false;
  306. //Determine if this line defines an index
  307. if ( in_array($token, array('primary', 'unique', 'fulltext')) ){
  308. $index_modifier = $token;
  309. $index = true;
  310. $token = strtolower(array_shift($pieces));
  311. }
  312. if ( in_array($token, array('index', 'key')) ){
  313. $index = true;
  314. $token = strtolower(array_shift($pieces));
  315. }
  316. //Determine column/index name
  317. $name = '';
  318. if ( $index ){
  319. //Names are optional for indexes; the INDEX/etc keyword can be immediately
  320. //followed by a column list (or index_type, but we're ignoring that possibility).
  321. if ( strpos($token, '(') === false ){
  322. $name = $token;
  323. } else {
  324. if ( $index_modifier == 'primary' ){
  325. $name = 'primary';
  326. }
  327. array_unshift($pieces, $token);
  328. }
  329. } else {
  330. $name = $token;
  331. }
  332. $name = strtolower(trim($name, '`'));
  333. $definition = compact('name', 'index', 'index_modifier');
  334. //Parse the rest of the line
  335. $remainder = implode(' ', $pieces);
  336. if ( $index ){
  337. $definition['columns'] = blcTableDelta::parse_index_column_list($remainder);
  338. //If the index doesn't have a name, use the name of the first column
  339. //(this is what MySQL does, but only when there isn't already an index with that name).
  340. if ( empty($definition['name']) ){
  341. $definition['name'] = $definition['columns'][0]['column_name'];
  342. }
  343. //Rebuild the index def. in a normalized form
  344. $definition['index_definition'] = blcTableDelta::generate_index_string($definition);
  345. } else {
  346. $column_def = blcTableDelta::parse_column_definition($remainder);
  347. $definition = array_merge($definition, $column_def);
  348. }
  349. return $definition;
  350. }
  351. /**
  352. * Parse the list of columns included in an index.
  353. *
  354. * This function returns a list of column descriptors. Each descriptor is
  355. * an associative array with the keys 'column_name', 'length' and 'order'.
  356. *
  357. * @param string $line
  358. * @return array Array of index columns
  359. */
  360. function parse_index_column_list($line){
  361. $line = preg_replace('@^\s*\(|\)\s*$@', '', $line); //Strip the braces that surround the column list
  362. $pieces = preg_split('@\s*,\s*@', $line);
  363. $columns = array();
  364. foreach($pieces as $piece){
  365. if ( preg_match('@`?(?P<column_name>[^\s`]+)`?(?:\s*\(\s*(?P<length>\d+)\s*\))?(?:\s+(?P<order>ASC|DESC))?@i', $piece, $matches) ){
  366. $column = array(
  367. 'column_name' => strtolower($matches['column_name']),
  368. 'length' => null,
  369. 'order' => null //unused; included for completeness
  370. );
  371. if ( isset($matches['length']) && is_numeric($matches['length']) ){
  372. $column['length'] = intval($matches['length']);
  373. }
  374. if ( isset($matches['order']) && !empty($matches['order']) ){
  375. $column['order'] = strtolower($matches['order']);
  376. }
  377. $columns[] = $column;
  378. };
  379. }
  380. return $columns;
  381. }
  382. /**
  383. * Parse column datatype and flags.
  384. *
  385. *
  386. * @param string $line
  387. * @return array
  388. */
  389. function parse_column_definition($line){
  390. $line = trim($line);
  391. //Extract datatype. This regexp is not entirely reliable - for example, it won't work
  392. //with enum fields where one of values contains brackets "()".
  393. $data_type = '';
  394. $regexp = '
  395. @
  396. (?P<type_name>^\w+)
  397. # followed by an optional length or a list of enum values
  398. (?:\s*
  399. \(
  400. \s* (?P<length>[^()]+) \s*
  401. \)
  402. )?
  403. # various type modifiers/keywords
  404. (?P<keywords>
  405. (?:\s+
  406. (?: BINARY | UNSIGNED | ZEROFILL )
  407. )*
  408. )?
  409. @xi';
  410. if ( preg_match($regexp, $line, $matches) ){
  411. $data_type = strtolower($matches['type_name']);
  412. if ( !empty($matches['length']) ){
  413. $data_type .= '(' . trim($matches['length']) . ')';
  414. }
  415. if ( !empty($matches['keywords']) ){
  416. $data_type .= preg_replace('@\s+@', ' ', $matches['keywords']); //Collapse spaces
  417. }
  418. $line = substr($line, strlen($data_type));
  419. }
  420. //Extract flags
  421. $null_allowed = !preg_match('@\sNOT\s+NULL\b@i', $line);
  422. $auto_increment = preg_match('@\sAUTO_INCREMENT\b@i', $line);
  423. //Got a default value?
  424. $default = null;
  425. if ( preg_match("@\sDEFAULT\s+('[^']*'|\"[^\"]*\"|\d+)@i", $line, $matches) ){
  426. $default = trim($matches[1], '"\'');
  427. }
  428. //Custom character set and/or collation?
  429. $charset = $collation = null;
  430. if ( preg_match('@ (?:\s CHARACTER \s+ SET \s+ (?P<charset>[^\s()]+) )? (?:\s COLLATE \s+ (?P<collation>[^\s()]+) )? @xi', $line, $matches) ){
  431. if ( isset($matches['charset']) ){
  432. $charset = $matches['charset'];
  433. }
  434. if ( isset($matches['collation']) ){
  435. $collation = $matches['collation'];
  436. }
  437. }
  438. //Generate the normalized column definition
  439. $column_definition = $data_type;
  440. if ( !empty($charset) ){
  441. $column_definition .= " CHARACTER SET {$charset}";
  442. }
  443. if ( !empty($collation) ){
  444. $column_definition .= " COLLATE {$collation}";
  445. }
  446. if ( !$null_allowed ){
  447. $column_definition .= " NOT NULL";
  448. }
  449. if ( !is_null($default) ){
  450. $column_definition .= " DEFAULT '{$default}'";
  451. }
  452. if ( $auto_increment ){
  453. $column_definition .= " AUTO_INCREMENT";
  454. }
  455. return compact('data_type', 'null_allowed', 'auto_increment', 'default', 'charset', 'collation', 'column_definition');
  456. }
  457. /**
  458. * Generate an index's definition string from its parsed representation.
  459. *
  460. * @param array $definition The return value of blcTableDelta::parse_create_definition()
  461. * @return string
  462. */
  463. function generate_index_string($definition){
  464. //Rebuild the index def. in a normalized form
  465. $index_definition = '';
  466. if ( !empty($definition['index_modifier']) ){
  467. $index_definition .= strtoupper($definition['index_modifier']) . ' ';
  468. }
  469. $index_definition .= 'KEY';
  470. if ( empty($definition['index_modifier']) || ($definition['index_modifier'] != 'primary') ){
  471. $index_definition .= ' `' . $definition['name'].'`';
  472. }
  473. $column_strings = array();
  474. foreach($definition['columns'] as $column){
  475. $c = '`' . $column['column_name'] . '`';
  476. if ( $column['length'] ){
  477. $c .= '('.$column['length'].')';
  478. }
  479. $column_strings[] = $c;
  480. }
  481. $index_definition .= ' (' . implode(', ', $column_strings) . ')';
  482. return $index_definition;
  483. }
  484. }
  485. ?>