PageRenderTime 67ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/wp-content/plugins/backupbuddy/lib/dbreplace/dbreplace.php

https://bitbucket.org/betaimages/chakalos
PHP | 401 lines | 236 code | 48 blank | 117 comment | 58 complexity | 6ddd898ea18b53b0896fe51d231a3cbb MD5 | raw file
Possible License(s): BSD-3-Clause
  1. <?php
  2. /**
  3. * pluginbuddy_dbreplace Class
  4. *
  5. * Handles replacement of data in a table/database, text or serialized. A database connection should be initialized before instantiation.
  6. *
  7. * Version: 1.0.0
  8. * Author: Dustin Bolton
  9. * Author URI: http://dustinbolton.com/
  10. *
  11. * @param $status_callback object Optional object containing the status() function for reporting back information.
  12. * @return null
  13. *
  14. */
  15. if (!class_exists("pluginbuddy_dbreplace")) {
  16. class pluginbuddy_dbreplace {
  17. var $_version = '1.0';
  18. /**
  19. * __construct()
  20. *
  21. * Default constructor. Sets up optional status() function class if applicable.
  22. *
  23. * @param reference &$status_callback [optional] Reference to the class containing the status() function for status updates.
  24. * @return null
  25. *
  26. */
  27. function __construct( ) {
  28. }
  29. /**
  30. * text()
  31. *
  32. * Replaces text within a table by specifying the table, rows to replace within and the old and new value(s).
  33. *
  34. * @param string $table Table to replace text in.
  35. * @param mixed $olds Old value(s) to find for replacement. May be a string or array of values.
  36. * @param mixed $news New value(s) to be replaced with. May be a string or array. If array there must be the same number of values as $olds.
  37. * @param mixed $rows Table row(s) to replace within. May be an array of tables.
  38. * @return null
  39. *
  40. */
  41. public function text( $table, $olds, $news, $rows ) {
  42. $rows_sql = array();
  43. if ( !is_array( $olds ) ) {
  44. $olds = array( $olds );
  45. }
  46. if ( !is_array( $news ) ) {
  47. $news = array( $news );
  48. }
  49. if ( !is_array( $rows ) ) {
  50. $rows = array( $rows );
  51. }
  52. // Prevent trying to replace data with the same data for performance.
  53. $this->remove_matching_array_elements( $olds, $news );
  54. foreach ( $rows as $row ) {
  55. $i = 0;
  56. foreach ( $olds as $old ) {
  57. $rows_sql[] = $row . " = replace( {$row}, '{$old}', '{$news[$i]}')";
  58. $i++;
  59. }
  60. }
  61. return mysql_query( "UPDATE `{$table}` SET " . implode( ',', $rows_sql ) . ";" );
  62. }
  63. /**
  64. * serialized()
  65. *
  66. * Replaces serialized text within a table by specifying the table, rows to replace within and the old and new value(s).
  67. *
  68. * @param string $table Table to replace text in.
  69. * @param mixed $olds Old value(s) to find for replacement. May be a string or array of values.
  70. * @param mixed $news New value(s) to be replaced with. May be a string or array. If array there must be the same number of values as $olds.
  71. * @param mixed $rows Table row(s) to replace within. May be an array of tables.
  72. * @return null
  73. *
  74. */
  75. public function serialized( $table, $olds, $news, $rows ) {
  76. if ( !is_array( $olds ) ) {
  77. $olds = array( $olds );
  78. }
  79. if ( !is_array( $news ) ) {
  80. $news = array( $news );
  81. }
  82. if ( !is_array( $rows ) ) {
  83. $rows = array( $rows );
  84. }
  85. // Prevent trying to replace data with the same data for performance.
  86. $this->remove_matching_array_elements( $olds, $news );
  87. $key_result = mysql_query( "show keys from {$table} WHERE Key_name='PRIMARY';" );
  88. if ( $key_result === false ) {
  89. pb_backupbuddy::status( 'details', 'Table `' . $table . '` does not exist; skipping migration of this table.' );
  90. return;
  91. }
  92. // No primary key found; unsafe to edit this table. @since 2.2.32.
  93. if ( mysql_num_rows( $key_result ) == 0 ) {
  94. pb_backupbuddy::status( 'message', 'Error #9029: Table `'. $table .'` does not contain a primary key; BackupBuddy cannot safely modify the contents of this table. Skipping migration of this table. (serialized()).' );
  95. return;
  96. }
  97. $key_result = mysql_fetch_array( $key_result );
  98. $primary_key = $key_result['Column_name'];
  99. unset( $key_result );
  100. $result = mysql_query( "SELECT `" . implode( '`,`', $rows ) . "`,`{$primary_key}` FROM `{$table}`");
  101. $updated = false;
  102. while ( $row = mysql_fetch_array( $result, MYSQL_ASSOC ) ) {
  103. $needs_update = false;
  104. $sql_update = array();
  105. foreach( $row as $column => $value ) {
  106. if ( $column != $primary_key ) {
  107. if ( false !== ( $edited_data = $this->replace_maybe_serialized( $value, $olds, $news ) ) ) { // Data changed.
  108. $needs_update = true;
  109. $sql_update[] = $column . "= '" . mysql_real_escape_string( $edited_data ) . "'";
  110. }
  111. } else {
  112. $primary_key_value = $value;
  113. }
  114. }
  115. if ( $needs_update === true ) {
  116. $updated = true;
  117. mysql_query( "UPDATE `{$table}` SET " . implode( ',', $sql_update ) . " WHERE `{$primary_key}` = '{$primary_key_value}' LIMIT 1" );
  118. }
  119. }
  120. if ( $updated === true ) {
  121. pb_backupbuddy::status( 'details', 'Updated serialized data in ' . $table . '.' );
  122. }
  123. }
  124. /**
  125. * replace_maybe_serialized()
  126. *
  127. * Replaces possibly serialized (or non-serialized) text if a change is needed. Returns false if there was no change.
  128. *
  129. * @param string $table Text (possibly serialized) to update.
  130. * @param mixed $olds Text to search for to replace. May be an array of strings to search for.
  131. * @param mixed $news New value(s) to be replaced with. May be a string or array. If array there must be the same number of values as $olds.
  132. * @return mixed Returns modified string data if serialized data was replaced. False if no change was made.
  133. *
  134. */
  135. function replace_maybe_serialized( $data, $olds, $news ) {
  136. if ( !is_array( $olds ) ) {
  137. $olds = array( $olds );
  138. }
  139. if ( !is_array( $news ) ) {
  140. $news = array( $news );
  141. }
  142. $type = '';
  143. $unserialized = false; // first assume not serialized data
  144. if ( is_serialized( $data ) ) { // check if this is serialized data
  145. $unserialized = @unserialize( $data ); // unserialise - if false is returned we won't try to process it as serialised.
  146. }
  147. if ( $unserialized !== false ) { // Serialized data.
  148. $type = 'serialized';
  149. $i = 0;
  150. foreach ( $olds as $old ) {
  151. $this->recursive_array_replace( $old, $news[$i], $unserialized );
  152. $i++;
  153. }
  154. $edited_data = serialize( $unserialized );
  155. } else { // Non-serialized data.
  156. $type = 'text';
  157. $edited_data = $data;
  158. $i = 0;
  159. foreach ( $olds as $old ) {
  160. $edited_data =str_ireplace( $old, $news[$i], $edited_data );
  161. $i++;
  162. }
  163. }
  164. // Return the results.
  165. if ( $data != $edited_data ) {
  166. return $edited_data;
  167. } else {
  168. return false;
  169. }
  170. }
  171. /**
  172. * bruteforce_table()
  173. *
  174. * !!! HANDLES SERIALIZED DATA !!!!
  175. * Replaces text, serialized or not, within the entire table. Bruteforce method iterates through every row & column in the entire table and replaces if needed.
  176. *
  177. * @param string $table Text (possibly serialized) to update.
  178. * @param mixed $olds Text to search for to replace. May be an array of strings to search for.
  179. * @param mixed $news New value(s) to be replaced with. May be a string or array. If array there must be the same number of values as $olds.
  180. * @return int Number of rows changed.
  181. *
  182. */
  183. function bruteforce_table( $table, $olds, $news ) {
  184. pb_backupbuddy::status( 'message', 'Starting brute force data migration for table `' . $table . '`...' );
  185. if ( !is_array( $olds ) ) {
  186. $olds = array( $olds );
  187. }
  188. if ( !is_array( $news ) ) {
  189. $news = array( $news );
  190. }
  191. $count_items_checked = 0;
  192. $count_items_changed = 0;
  193. $fields_list = mysql_query( "DESCRIBE `" . $table . "`" );
  194. $index_fields = ''; // Reset fields for each table.
  195. $column_name = '';
  196. $table_index = '';
  197. $i = 0;
  198. $found_primary_key = false;
  199. while ( $field_rows = mysql_fetch_array( $fields_list ) ) {
  200. $column_name[$i++] = $field_rows['Field'];
  201. if ( $field_rows['Key'] == 'PRI' ) {
  202. $table_index[$i] = true;
  203. $found_primary_key = true;
  204. }
  205. }
  206. // Skips migration of this table if there is no primary key. Modifying on any other key is not safe. mysql automatically returns a PRIMARY if a UNIQUE non-primary is found according to http://dev.mysql.com/doc/refman/5.1/en/create-table.html @since 2.2.32.
  207. if ( $found_primary_key === false ) {
  208. pb_backupbuddy::status( 'message', 'Error #9029: Table `'. $table .'` does not contain a primary key; BackupBuddy cannot safely modify the contents of this table. Skipping migration of this table. (bruteforce_table()).' );
  209. return false;
  210. }
  211. $data = mysql_query( "SELECT * FROM `" . $table . "`" );
  212. if (!$data) {
  213. pb_backupbuddy::status( 'error', 'ERROR #44545343 ... SQL ERROR: ' . mysql_error() );
  214. }
  215. $row_loop = 0;
  216. while ( $row = mysql_fetch_array( $data ) ) {
  217. $need_to_update = false;
  218. $UPDATE_SQL = 'UPDATE `' . $table . '` SET ';
  219. $WHERE_SQL = ' WHERE ';
  220. $j = 0;
  221. foreach ( $column_name as $current_column ) {
  222. $j++;
  223. $count_items_checked++;
  224. $row_loop++;
  225. if ( $row_loop > 5000 ) {
  226. pb_backupbuddy::status( 'message', 'Working...' );
  227. $row_loop = 0;
  228. }
  229. $data_to_fix = $row[$current_column];
  230. if ( false !== ( $edited_data = $this->replace_maybe_serialized( $data_to_fix, $olds, $news ) ) ) { // no change needed
  231. $count_items_changed++;
  232. if ( $need_to_update != false ) { // If this isn't our first time here, we need to add a comma.
  233. $UPDATE_SQL = $UPDATE_SQL . ',';
  234. }
  235. $UPDATE_SQL = $UPDATE_SQL . ' ' . $current_column . ' = "' . mysql_real_escape_string( $edited_data ) . '"';
  236. $need_to_update = true; // Only set if we need to update - avoids wasted UPDATE statements.
  237. }
  238. if ( isset( $table_index[$j] ) ) {
  239. $WHERE_SQL = $WHERE_SQL . '`' . $current_column . '` = "' . $row[$current_column] . '" AND ';
  240. }
  241. }
  242. if ( $need_to_update ) {
  243. $WHERE_SQL = substr( $WHERE_SQL , 0, -4 ); // Strip off the excess AND - the easiest way to code this without extra flags, etc.
  244. $UPDATE_SQL = $UPDATE_SQL . $WHERE_SQL;
  245. $result = mysql_query( $UPDATE_SQL );
  246. if ( !$result ) {
  247. pb_backupbuddy::status( 'error', 'ERROR: mysql error updating db: ' . mysql_error() . '. SQL Query: ' . htmlentities( $UPDATE_SQL ) );
  248. }
  249. }
  250. }
  251. unset( $main_result );
  252. pb_backupbuddy::status( 'message', 'Brute force data migration for table `' . $table . '` complete. Checked ' . $count_items_checked . ' items; ' . $count_items_changed . ' changed.' );
  253. return $count_items_changed;
  254. }
  255. /**
  256. * recursive_array_replace()
  257. *
  258. * Recursively replace text in an array, stepping through arrays within arrays as needed.
  259. *
  260. * @param string $find Text to find.
  261. * @param string $replace Text to replace found text with.
  262. * @param reference &$data Pass the variable to change the data within.
  263. * @return boolean Always true currently.
  264. *
  265. */
  266. public function recursive_array_replace( $find, $replace, &$data ) {
  267. if ( is_array( $data ) ) {
  268. foreach ( $data as $key => $value ) {
  269. if ( is_array( $value ) ) {
  270. $this->recursive_array_replace( $find, $replace, $data[$key] );
  271. } else {
  272. // Have to check if it's string to ensure no switching to string for booleans/numbers/nulls - don't need any nasty conversions.
  273. if ( is_string( $value ) ) $data[$key] = str_replace( $find, $replace, $value );
  274. }
  275. }
  276. } else {
  277. if ( is_string( $data ) ) $data = str_replace( $find, $replace, $data );
  278. }
  279. }
  280. /**
  281. * Check value to find if it was serialized.
  282. *
  283. * If $data is not an string, then returned value will always be false.
  284. * Serialized data is always a string.
  285. *
  286. * Courtesy WordPress; since WordPress 2.0.5.
  287. *
  288. * @param mixed $data Value to check to see if was serialized.
  289. * @return bool False if not serialized and true if it was.
  290. */
  291. function is_serialized( $data ) {
  292. // if it isn't a string, it isn't serialized
  293. if ( ! is_string( $data ) )
  294. return false;
  295. $data = trim( $data );
  296. if ( 'N;' == $data )
  297. return true;
  298. $length = strlen( $data );
  299. if ( $length < 4 )
  300. return false;
  301. if ( ':' !== $data[1] )
  302. return false;
  303. $lastc = $data[$length-1];
  304. if ( ';' !== $lastc && '}' !== $lastc )
  305. return false;
  306. $token = $data[0];
  307. switch ( $token ) {
  308. case 's' :
  309. if ( '"' !== $data[$length-2] )
  310. return false;
  311. case 'a' :
  312. case 'O' :
  313. return (bool) preg_match( "/^{$token}:[0-9]+:/s", $data );
  314. case 'b' :
  315. case 'i' :
  316. case 'd' :
  317. return (bool) preg_match( "/^{$token}:[0-9.E-]+;\$/", $data );
  318. }
  319. return false;
  320. }
  321. /**
  322. * remove_matching_array_elements()
  323. *
  324. * Removes identical elements (same index and value) from both arrays where they match.
  325. *
  326. * Ex:
  327. * // Before:
  328. * $a = array( 'apple', 'banana', 'carrot' );
  329. * $b = array( 'apple', 'beef', 'cucumber' );
  330. * remove_matching_array_elements( $a, $b );
  331. * // After:
  332. * $a = array( 'banana', 'carrot' );
  333. * $b = array( 'beef', 'cucumber' );
  334. *
  335. * @param array &$a First array to compare with second. (reference)
  336. * @param array &$b Second array to compare with first. (reference)
  337. * @return null Arrays passed are updated as they are passed by reference.
  338. *
  339. */
  340. function remove_matching_array_elements( &$a, &$b ) {
  341. $sizeof = sizeof( $a );
  342. for( $i=0; $i < $sizeof; $i++ ) {
  343. if ( $a[$i] == $b[$i] ) {
  344. unset( $a[$i] );
  345. unset( $b[$i] );
  346. }
  347. }
  348. $a = array_merge( $a ); // Reset numbering of keys.
  349. $b = array_merge( $b ); // Reset numbering of keys.
  350. }
  351. } // end pluginbuddy_dbreplace class.
  352. }
  353. ?>