PageRenderTime 41ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 0ms

/wp-content/plugins/search-and-replace/inc/Database/Replace.php

https://gitlab.com/iamgraeme/royalmile
PHP | 368 lines | 218 code | 55 blank | 95 comment | 20 complexity | 5650392f78a9148a81da533ae4dcd5ed MD5 | raw file
  1. <?php
  2. namespace Inpsyde\SearchReplace\Database;
  3. use Inpsyde\SearchReplace\Service;
  4. /**
  5. * Class Replace
  6. * runs search & replace on a database
  7. * adapted from: https://github.com/interconnectit/Search-Replace-DB/blob/master/srdb.class.php
  8. *
  9. * @package Inpsyde\SearchReplace\Database
  10. */
  11. class Replace {
  12. /**
  13. * the search string
  14. *
  15. * @var
  16. */
  17. protected $search;
  18. /**
  19. * the replacement string
  20. *
  21. * @var
  22. */
  23. protected $replace;
  24. /**
  25. * The Database Interface Object
  26. *
  27. * @type Manager
  28. * @var
  29. */
  30. protected $dbm;
  31. /**
  32. * Count of rows to be replaced at a time
  33. *
  34. * @var int
  35. */
  36. protected $page_size = 100;
  37. /**
  38. * @var bool - set if dry run
  39. */
  40. protected $dry_run = TRUE;
  41. /**
  42. * Replace constructor.
  43. *
  44. * @param Manager $dbm
  45. */
  46. public function __construct( Manager $dbm ) {
  47. $this->dbm = $dbm;
  48. }
  49. /**
  50. * The main loop triggered in step 5. Up here to keep it out of the way of the
  51. * HTML. This walks every table in the db that was selected in step 3 and then
  52. * walks every row and column replacing all occurrences of a string with another.
  53. * We split large tables into blocks (size is set via $page_size)when dealing with them to save
  54. * on memory consumption.
  55. *
  56. * @param string $search What we want to replace
  57. * @param string $replace What we want to replace it with.
  58. * @param string $tables The name of the table we want to look at.
  59. *
  60. * @return array Collection of information gathered during the run.
  61. */
  62. public function run_search_replace( $search, $replace, $tables ) {
  63. if ( $search === $replace ){
  64. return new \WP_Error( 'error', __( "Search and replace pattern can't be the same!" ) );
  65. }
  66. $execution_time = new Service\MaxExecutionTime();
  67. $execution_time->set();
  68. $report = array(
  69. 'errors' => NULL,
  70. 'changes' => array(),
  71. 'tables' => '0',
  72. 'changes_count' => '0',
  73. );
  74. foreach ( (array) $tables as $table ) {
  75. //count tables
  76. $report [ 'tables' ] ++;
  77. $table_report = $this->replace_values( $search, $replace, $table );
  78. //log changes if any
  79. if ( 0 !== $table_report[ 'change' ] ) {
  80. $report[ 'changes' ][ $table ] = $table_report;
  81. $report [ 'changes_count' ] += $table_report[ 'change' ];
  82. }
  83. }
  84. $execution_time->restore();
  85. return $report;
  86. }
  87. public function replace_values( $search = '', $replace = '', $table ) {
  88. $table_report = array(
  89. 'table_name' => $table,
  90. 'rows' => 0,
  91. 'change' => 0,
  92. 'changes' => array(),
  93. 'updates' => 0,
  94. 'start' => microtime(),
  95. 'end' => microtime(),
  96. 'errors' => array(),
  97. );
  98. // check we have a search string, bail if not
  99. if ( empty( $search ) ) {
  100. $table_report[ 'errors' ][] = 'Search string is empty';
  101. return $table_report;
  102. }
  103. //split columns array in primary key string and columns array
  104. $columns = $this->dbm->get_columns( $table );
  105. $primary_key = $columns[ 0 ];
  106. $columns = $columns[ 1 ];
  107. if ( NULL === $primary_key ) {
  108. array_push(
  109. $table_report[ 'errors' ],
  110. "The table \"{$table}\" has no primary key. Changes will have to be made manually.",
  111. 'results'
  112. );
  113. return $table_report;
  114. }
  115. $table_report[ 'start' ] = microtime();
  116. // Count the number of rows we have in the table if large we'll split into blocks, This is a mod from Simon Wheatley
  117. $row_count = $this->dbm->get_rows( $table );
  118. $page_size = $this->page_size;
  119. $pages = ceil( $row_count / $page_size );
  120. for ( $page = 0; $page < $pages; $page ++ ) {
  121. $start = $page * $page_size;
  122. // Grab the content of the table
  123. $data = $this->dbm->get_table_content( $table, $start, $page_size );
  124. if ( ! $data ) {
  125. $table_report[ 'errors' ][] = 'no data in table ' . $table;
  126. }
  127. foreach ( $data as $row ) {
  128. $table_report[ 'rows' ] ++;
  129. $update_sql = array();
  130. $where_sql = array();
  131. $update = FALSE;
  132. foreach ( $columns as $column ) {
  133. $data_to_fix = $row[ $column ];
  134. if ( $column === $primary_key ) {
  135. $where_sql[] = $column . ' = "' . $this->mysql_escape_mimic( $data_to_fix ) . '"';
  136. continue;
  137. }
  138. /* // exclude cols
  139. if ( in_array( $column, $this->exclude_cols ) )
  140. continue;
  141. // include cols
  142. if ( ! empty( $this->include_cols ) && ! in_array( $column, $this->include_cols ) )
  143. continue;*/
  144. // Run a search replace on the data that'll respect the serialisation.
  145. $edited_data = $this->recursive_unserialize_replace( $search, $replace, $data_to_fix );
  146. // Something was changed
  147. if ( $edited_data !== $data_to_fix ) {
  148. $table_report[ 'change' ] ++;
  149. // log changes
  150. //TODO : does it work with non UTF-8 encodings?
  151. $table_report[ 'changes' ][] = array(
  152. 'row' => $table_report[ 'rows' ],
  153. 'column' => $column,
  154. 'from' => $data_to_fix,
  155. 'to' => $edited_data,
  156. );
  157. $update_sql[] = $column . ' = "' . $this->mysql_escape_mimic( $edited_data ) . '"';
  158. $update = TRUE;
  159. }
  160. }
  161. // Determine what to do with updates.
  162. if ( TRUE === $this->dry_run ) {
  163. // Don't do anything if a dry run
  164. } elseif ( $update && ! empty( $where_sql ) ) {
  165. // If there are changes to make, run the query.
  166. $result = $this->dbm->update( $table, $update_sql, $where_sql );
  167. if ( ! $result ) {
  168. $table_report[ 'errors' ][] = sprintf(
  169. __( 'Error updating row: %d.', 'search-and-replace' ),
  170. $row
  171. );
  172. } else {
  173. $table_report[ 'updates' ] ++;
  174. }
  175. }
  176. }
  177. }
  178. $table_report[ 'end' ] = microtime( TRUE );
  179. $this->dbm->flush();
  180. return $table_report;
  181. }
  182. /**
  183. * Take a serialised array and unserialize it replacing elements as needed and
  184. * unserializing any subordinate arrays and performing the replace on those too.
  185. *
  186. * @param string $from String we're looking to replace.
  187. * @param string $to What we want it to be replaced with
  188. * @param array|string|object $data Used to pass any subordinate arrays back to in.
  189. * @param bool $serialised Does the array passed via $data need serialising.
  190. *
  191. * @return array The original array with all elements replaced as needed.
  192. */
  193. public function recursive_unserialize_replace( $from = '', $to = '', $data = '', $serialised = FALSE ) {
  194. // some unserialized data cannot be re-serialised eg. SimpleXMLElements
  195. try {
  196. if ( is_string( $data ) && ( $unserialized = @unserialize( $data ) ) !== FALSE ) {
  197. $data = $this->recursive_unserialize_replace( $from, $to, $unserialized, TRUE );
  198. } elseif ( is_array( $data ) ) {
  199. $_tmp = array();
  200. foreach ( $data as $key => $value ) {
  201. $_tmp[ $key ] = $this->recursive_unserialize_replace( $from, $to, $value, FALSE );
  202. }
  203. $data = $_tmp;
  204. unset( $_tmp );
  205. } // Submitted by Tina Matter
  206. elseif ( is_object( $data ) ) {
  207. // $data_class = get_class( $data );
  208. $_tmp = $data; // new $data_class( );
  209. $props = get_object_vars( $data );
  210. foreach ( $props as $key => $value ) {
  211. $_tmp->$key = $this->recursive_unserialize_replace( $from, $to, $value, FALSE );
  212. }
  213. $data = $_tmp;
  214. unset( $_tmp );
  215. } else {
  216. if ( is_string( $data ) ) {
  217. $data = str_replace( $from, $to, $data );
  218. }
  219. }
  220. if ( $serialised ) {
  221. return serialize( $data );
  222. }
  223. }
  224. catch ( Exception $error ) {
  225. $this->add_error( $error->getMessage(), 'results' );
  226. }
  227. return $data;
  228. }
  229. /**
  230. * Checks if the submitted string is a JSON object
  231. *
  232. * @param $string
  233. * @param bool|FALSE $strict
  234. *
  235. * @return bool
  236. */
  237. protected function is_json( $string, $strict = FALSE ) {
  238. $json = @json_decode( $string, TRUE );
  239. if ( $strict === TRUE && ! is_array( $json ) ) {
  240. return FALSE;
  241. }
  242. return ! ( $json === NULL || $json === FALSE );
  243. }
  244. /**
  245. * Mimics the mysql_real_escape_string function. Adapted from a post by 'feedr' on php.net.
  246. *
  247. * @link http://php.net/manual/en/function.mysql-real-escape-string.php#101248
  248. * @access public
  249. *
  250. * @param array|string $input The string to escape.
  251. *
  252. * @return string
  253. */
  254. public function mysql_escape_mimic( $input ) {
  255. if ( is_array( $input ) ) {
  256. return array_map( __METHOD__, $input );
  257. }
  258. if ( ! empty( $input ) && is_string( $input ) ) {
  259. return str_replace(
  260. array( '\\', "\0", "\n", "\r", "'", '"', "\x1a" ),
  261. array( '\\\\', '\\0', '\\n', '\\r', "\\'", '\\"', '\\Z' ),
  262. $input
  263. );
  264. }
  265. return $input;
  266. }
  267. /**
  268. * Sets the dry run option.
  269. *
  270. * @param bool $state : TRUE for dry run, FALSE for writing changes to DB
  271. *
  272. * @return bool
  273. */
  274. public function set_dry_run( $state ) {
  275. if ( is_bool( $state ) ) {
  276. $this->dry_run = $state;
  277. }
  278. return $state;
  279. }
  280. /**
  281. * Returns true, if dry run, false if not
  282. *
  283. * @return bool
  284. */
  285. public function get_dry_run() {
  286. return $this->dry_run;
  287. }
  288. }