PageRenderTime 46ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

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

https://bitbucket.org/anneivycat/ebcookhouse
PHP | 414 lines | 244 code | 52 blank | 118 comment | 60 complexity | d21733f8d91122418fead38e7a30e8e5 MD5 | raw file
  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. * Note: As of BB v3.2.x supports double serialized data.
  129. *
  130. * @param string $table Text (possibly serialized) to update.
  131. * @param mixed $olds Text to search for to replace. May be an array of strings to search for.
  132. * @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.
  133. * @return mixed Returns modified string data if serialized data was replaced. False if no change was made.
  134. *
  135. */
  136. function replace_maybe_serialized( $data, $olds, $news ) {
  137. if ( !is_array( $olds ) ) {
  138. $olds = array( $olds );
  139. }
  140. if ( !is_array( $news ) ) {
  141. $news = array( $news );
  142. }
  143. $type = '';
  144. $unserialized = false; // first assume not serialized data
  145. if ( is_serialized( $data ) ) { // check if this is serialized data
  146. $unserialized = @unserialize( $data ); // unserialise - if false is returned we won't try to process it as serialised.
  147. }
  148. if ( $unserialized !== false ) { // Serialized data.
  149. $type = 'serialized';
  150. $double_serialized = false;
  151. if ( is_serialized( $unserialized ) ) { // double-serialized data (opposite of a double rainbow). Some plugins seem to double-serialize for some unknown wacky reason...
  152. $unserialized = @unserialize( $unserialized ); // unserialise - if false is returned we won't try to process it as serialised.
  153. $double_serialized = true;
  154. }
  155. $i = 0;
  156. foreach ( $olds as $old ) {
  157. $this->recursive_array_replace( $old, $news[$i], $unserialized );
  158. $i++;
  159. }
  160. $edited_data = serialize( $unserialized );
  161. if ( true === $double_serialized ) {
  162. $edited_data = serialize( $edited_data );
  163. }
  164. } else { // Non-serialized data.
  165. $type = 'text';
  166. $edited_data = $data;
  167. $i = 0;
  168. foreach ( $olds as $old ) {
  169. $edited_data =str_ireplace( $old, $news[$i], $edited_data );
  170. $i++;
  171. }
  172. }
  173. // Return the results.
  174. if ( $data != $edited_data ) {
  175. return $edited_data;
  176. } else {
  177. return false;
  178. }
  179. }
  180. /**
  181. * bruteforce_table()
  182. *
  183. * !!! HANDLES SERIALIZED DATA !!!!
  184. * Replaces text, serialized or not, within the entire table. Bruteforce method iterates through every row & column in the entire table and replaces if needed.
  185. *
  186. * @param string $table Text (possibly serialized) to update.
  187. * @param mixed $olds Text to search for to replace. May be an array of strings to search for.
  188. * @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.
  189. * @return int Number of rows changed.
  190. *
  191. */
  192. function bruteforce_table( $table, $olds, $news ) {
  193. pb_backupbuddy::status( 'message', 'Starting brute force data migration for table `' . $table . '`...' );
  194. if ( !is_array( $olds ) ) {
  195. $olds = array( $olds );
  196. }
  197. if ( !is_array( $news ) ) {
  198. $news = array( $news );
  199. }
  200. $count_items_checked = 0;
  201. $count_items_changed = 0;
  202. $fields_list = mysql_query( "DESCRIBE `" . $table . "`" );
  203. $index_fields = ''; // Reset fields for each table.
  204. $column_name = '';
  205. $table_index = '';
  206. $i = 0;
  207. $found_primary_key = false;
  208. while ( $field_rows = mysql_fetch_array( $fields_list ) ) {
  209. $column_name[$i++] = $field_rows['Field'];
  210. if ( $field_rows['Key'] == 'PRI' ) {
  211. $table_index[$i] = true;
  212. $found_primary_key = true;
  213. }
  214. }
  215. // 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.
  216. if ( $found_primary_key === false ) {
  217. 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()).' );
  218. return false;
  219. }
  220. $data = mysql_query( "SELECT * FROM `" . $table . "`" );
  221. if (!$data) {
  222. pb_backupbuddy::status( 'error', 'ERROR #44545343 ... SQL ERROR: ' . mysql_error() );
  223. }
  224. $row_loop = 0;
  225. while ( $row = mysql_fetch_array( $data ) ) {
  226. $need_to_update = false;
  227. $UPDATE_SQL = 'UPDATE `' . $table . '` SET ';
  228. $WHERE_SQL = ' WHERE ';
  229. $j = 0;
  230. foreach ( $column_name as $current_column ) {
  231. $j++;
  232. $count_items_checked++;
  233. $row_loop++;
  234. if ( $row_loop > 20000 ) {
  235. pb_backupbuddy::status( 'message', 'Working... ' . $count_items_checked . ' rows checked.' );
  236. $row_loop = 0;
  237. }
  238. $data_to_fix = $row[$current_column];
  239. if ( false !== ( $edited_data = $this->replace_maybe_serialized( $data_to_fix, $olds, $news ) ) ) { // no change needed
  240. $count_items_changed++;
  241. if ( $need_to_update != false ) { // If this isn't our first time here, we need to add a comma.
  242. $UPDATE_SQL = $UPDATE_SQL . ',';
  243. }
  244. $UPDATE_SQL = $UPDATE_SQL . ' ' . $current_column . ' = "' . mysql_real_escape_string( $edited_data ) . '"';
  245. $need_to_update = true; // Only set if we need to update - avoids wasted UPDATE statements.
  246. }
  247. if ( isset( $table_index[$j] ) ) {
  248. $WHERE_SQL = $WHERE_SQL . '`' . $current_column . '` = "' . $row[$current_column] . '" AND ';
  249. }
  250. }
  251. if ( $need_to_update ) {
  252. $WHERE_SQL = substr( $WHERE_SQL , 0, -4 ); // Strip off the excess AND - the easiest way to code this without extra flags, etc.
  253. $UPDATE_SQL = $UPDATE_SQL . $WHERE_SQL;
  254. $result = mysql_query( $UPDATE_SQL );
  255. if ( !$result ) {
  256. pb_backupbuddy::status( 'error', 'ERROR: mysql error updating db: ' . mysql_error() . '. SQL Query: ' . htmlentities( $UPDATE_SQL ) );
  257. }
  258. }
  259. }
  260. unset( $main_result );
  261. pb_backupbuddy::status( 'message', 'Brute force data migration for table `' . $table . '` complete. Checked ' . $count_items_checked . ' items; ' . $count_items_changed . ' changed.' );
  262. return $count_items_changed;
  263. }
  264. /**
  265. * recursive_array_replace()
  266. *
  267. * Recursively replace text in an array, stepping through arrays within arrays as needed.
  268. *
  269. * @param string $find Text to find.
  270. * @param string $replace Text to replace found text with.
  271. * @param reference &$data Pass the variable to change the data within.
  272. * @return boolean Always true currently.
  273. *
  274. */
  275. public function recursive_array_replace( $find, $replace, &$data ) {
  276. if ( is_array( $data ) ) {
  277. foreach ( $data as $key => $value ) {
  278. if ( is_array( $value ) ) {
  279. $this->recursive_array_replace( $find, $replace, $data[$key] );
  280. } else {
  281. // Have to check if it's string to ensure no switching to string for booleans/numbers/nulls - don't need any nasty conversions.
  282. if ( is_string( $value ) ) $data[$key] = str_replace( $find, $replace, $value );
  283. }
  284. }
  285. } else {
  286. if ( is_string( $data ) ) $data = str_replace( $find, $replace, $data );
  287. }
  288. }
  289. /**
  290. * Check value to find if it was serialized.
  291. *
  292. * If $data is not an string, then returned value will always be false.
  293. * Serialized data is always a string.
  294. *
  295. * Courtesy WordPress; since WordPress 2.0.5.
  296. *
  297. * @param mixed $data Value to check to see if was serialized.
  298. * @return bool False if not serialized and true if it was.
  299. */
  300. function is_serialized( $data ) {
  301. // if it isn't a string, it isn't serialized
  302. if ( ! is_string( $data ) )
  303. return false;
  304. $data = trim( $data );
  305. if ( 'N;' == $data )
  306. return true;
  307. $length = strlen( $data );
  308. if ( $length < 4 )
  309. return false;
  310. if ( ':' !== $data[1] )
  311. return false;
  312. $lastc = $data[$length-1];
  313. if ( ';' !== $lastc && '}' !== $lastc )
  314. return false;
  315. $token = $data[0];
  316. switch ( $token ) {
  317. case 's' :
  318. if ( '"' !== $data[$length-2] )
  319. return false;
  320. case 'a' :
  321. case 'O' :
  322. return (bool) preg_match( "/^{$token}:[0-9]+:/s", $data );
  323. case 'b' :
  324. case 'i' :
  325. case 'd' :
  326. return (bool) preg_match( "/^{$token}:[0-9.E-]+;\$/", $data );
  327. }
  328. return false;
  329. }
  330. /**
  331. * remove_matching_array_elements()
  332. *
  333. * Removes identical elements (same index and value) from both arrays where they match.
  334. *
  335. * Ex:
  336. * // Before:
  337. * $a = array( 'apple', 'banana', 'carrot' );
  338. * $b = array( 'apple', 'beef', 'cucumber' );
  339. * remove_matching_array_elements( $a, $b );
  340. * // After:
  341. * $a = array( 'banana', 'carrot' );
  342. * $b = array( 'beef', 'cucumber' );
  343. *
  344. * @param array &$a First array to compare with second. (reference)
  345. * @param array &$b Second array to compare with first. (reference)
  346. * @return null Arrays passed are updated as they are passed by reference.
  347. *
  348. */
  349. function remove_matching_array_elements( &$a, &$b ) {
  350. $sizeof = sizeof( $a );
  351. for( $i=0; $i < $sizeof; $i++ ) {
  352. if ( $a[$i] == $b[$i] ) {
  353. unset( $a[$i] );
  354. unset( $b[$i] );
  355. }
  356. }
  357. $a = array_merge( $a ); // Reset numbering of keys.
  358. $b = array_merge( $b ); // Reset numbering of keys.
  359. }
  360. } // end pluginbuddy_dbreplace class.
  361. }
  362. ?>