WP Staging – DB & File Duplicator & Migration - Version 2.6.7

Version Description

  • Fix: Update function adds duplicate string to internal urls like https://example.com/staging/staging/wp-content/*
  • New: Support for WP 5.3.2
Download this release

Release Info

Developer ReneHermi
Plugin Icon 128x128 WP Staging – DB & File Duplicator & Migration
Version 2.6.7
Comparing to
See all releases

Code changes from version 2.6.6 to 2.6.7

Backend/Administrator.php CHANGED
@@ -151,7 +151,7 @@ class Administrator extends InjectionAware {
151
  */
152
  public function upgrade()
153
  {
154
- if (defined('WPSTGPRO_VERSION')) {
155
  $upgrade = new Pro\Upgrade\Upgrade();
156
  } else {
157
  $upgrade = new Upgrade\Upgrade();
151
  */
152
  public function upgrade()
153
  {
154
+ if (defined('WPSTGPRO_VERSION') && class_exists('WPStaging\Backend\Pro\Upgrade\Upgrade')) {
155
  $upgrade = new Pro\Upgrade\Upgrade();
156
  } else {
157
  $upgrade = new Upgrade\Upgrade();
Backend/Modules/Jobs/Delete.php CHANGED
@@ -148,7 +148,10 @@ class Delete extends Job {
148
 
149
  $stagingPrefix = $this->getStagingPrefix();
150
 
151
- $tables = $this->wpdb->get_results( "SHOW TABLE STATUS LIKE '{$stagingPrefix}%'" );
 
 
 
152
 
153
  $this->tables = array();
154
 
148
 
149
  $stagingPrefix = $this->getStagingPrefix();
150
 
151
+ // Escape "_" to allow searching for that character
152
+ $prefix = wpstg_replace_last_match('_', '\_', $stagingPrefix);
153
+
154
+ $tables = $this->wpdb->get_results( "SHOW TABLE STATUS LIKE '{$prefix}%'" );
155
 
156
  $this->tables = array();
157
 
Backend/Modules/Jobs/Multisite/SearchReplace.php CHANGED
@@ -3,7 +3,7 @@
3
  namespace WPStaging\Backend\Modules\Jobs\Multisite;
4
 
5
  // No Direct Access
6
- if( !defined( "WPINC" ) ) {
7
  die;
8
  }
9
 
@@ -17,13 +17,14 @@ use WPStaging\Backend\Modules\Jobs\JobExecutable;
17
  * Class Database
18
  * @package WPStaging\Backend\Modules\Jobs
19
  */
20
- class SearchReplace extends JobExecutable {
 
21
 
22
  /**
23
  * @var int
24
  */
25
  private $total = 0;
26
-
27
  /**
28
  * @var \WPDB
29
  */
@@ -35,7 +36,7 @@ class SearchReplace extends JobExecutable {
35
  */
36
  private $stagingDb;
37
 
38
- /**
39
  *
40
  * @var string
41
  */
@@ -55,37 +56,40 @@ class SearchReplace extends JobExecutable {
55
 
56
  /**
57
  * The prefix of the new database tables which are used for the live site after updating tables
58
- * @var string
59
  */
60
  public $tmpPrefix;
61
 
62
- /**
63
  * Initialize
64
  */
65
- public function initialize() {
66
- $this->total = count( $this->options->tables );
67
- $this->stagingDb = $this->getStagingDB();
68
- $this->productionDb = WPStaging::getInstance()->get( "wpdb" );
69
- $this->tmpPrefix = $this->options->prefix;
70
- $this->strings = new Strings();
71
- $this->sourceHostname = $this->getSourceHostname();
 
72
  $this->destinationHostname = $this->getDestinationHostname();
73
  }
74
 
75
  /**
76
  * Get database object to interact with
77
  */
78
- private function getStagingDB() {
79
- if(empty($this->options->databaseUser) || empty($this->options->databasePassword) || empty($this->options->databaseDatabase) || empty($this->options->databaseServer) ){
80
- return null;
81
- }
82
- return new \wpdb( $this->options->databaseUser, $this->options->databasePassword, $this->options->databaseDatabase, $this->options->databaseServer );
 
83
  }
84
 
85
- public function start() {
 
86
  // Skip job. Nothing to do
87
- if( $this->options->totalSteps === 0 ) {
88
- $this->prepareResponse( true, false );
89
  }
90
 
91
  $this->run();
@@ -93,14 +97,15 @@ class SearchReplace extends JobExecutable {
93
  // Save option, progress
94
  $this->saveOptions();
95
 
96
- return ( object ) $this->response;
97
  }
98
 
99
  /**
100
  * Calculate Total Steps in This Job and Assign It to $this->options->totalSteps
101
  * @return void
102
  */
103
- protected function calculateTotalSteps() {
 
104
  $this->options->totalSteps = $this->total;
105
  }
106
 
@@ -109,31 +114,32 @@ class SearchReplace extends JobExecutable {
109
  * Returns false when over threshold limits are hit or when the job is done, true otherwise
110
  * @return bool
111
  */
112
- protected function execute() {
 
113
  // Over limits threshold
114
- if( $this->isOverThreshold() ) {
115
  // Prepare response and save current progress
116
- $this->prepareResponse( false, false );
117
  $this->saveOptions();
118
  return false;
119
  }
120
 
121
  // No more steps, finished
122
- if( $this->options->currentStep > $this->total || !isset( $this->options->tables[$this->options->currentStep] ) ) {
123
- $this->prepareResponse( true, false );
124
  return false;
125
  }
126
 
127
  // Table is excluded
128
- if( in_array( $this->options->tables[$this->options->currentStep], $this->options->excludedTables ) ) {
129
  $this->prepareResponse();
130
  return true;
131
  }
132
 
133
  // Search & Replace
134
- if( !$this->stopExecution() && !$this->updateTable( $this->options->tables[$this->options->currentStep] ) ) {
135
  // Prepare Response
136
- $this->prepareResponse( false, false );
137
 
138
  // Not finished
139
  return true;
@@ -151,9 +157,10 @@ class SearchReplace extends JobExecutable {
151
  * Stop Execution immediately
152
  * return mixed bool | json
153
  */
154
- private function stopExecution() {
155
- if( $this->productionDb->prefix == $this->tmpPrefix ) {
156
- $this->returnException( 'Fatal Error 9: Prefix ' . $this->productionDb->prefix . ' is used for the live site hence it can not be used for the staging site as well. Please ask support@wp-staging.com how to resolve this.' );
 
157
  }
158
  return false;
159
  }
@@ -163,20 +170,21 @@ class SearchReplace extends JobExecutable {
163
  * @param string $tableName
164
  * @return bool
165
  */
166
- private function updateTable( $tableName ) {
167
- $strings = new Strings();
168
- $table = $strings->str_replace_first( $this->productionDb->prefix, '', $tableName );
 
169
  $newTableName = $this->tmpPrefix . $table;
170
 
171
  // Save current job
172
- $this->setJob( $newTableName );
173
 
174
  // Beginning of the job
175
- if( !$this->startJob( $newTableName, $tableName ) ) {
176
  return true;
177
  }
178
  // Copy data
179
- $this->startReplace( $newTableName );
180
 
181
  // Finish the step
182
  return $this->finishStep();
@@ -186,49 +194,64 @@ class SearchReplace extends JobExecutable {
186
  * Get source Hostname depending on wheather WP has been installed in sub dir or not
187
  * @return type
188
  */
189
- private function getSourceHostname() {
 
190
 
191
- if( $this->isSubDir() ) {
192
- return trailingslashit( $this->multisiteHomeUrlWithoutScheme ) . '/' . $this->getSubDir();
193
  }
194
  return $this->multisiteHomeUrlWithoutScheme;
195
  }
196
 
197
  /**
198
- * Get destination Hostname depending on wheather WP has been installed in sub dir or not
199
- * Retun host name without scheme
200
- * @return type
201
  */
202
- private function getDestinationHostname() {
 
203
 
204
- if( !empty( $this->options->cloneHostname ) ) {
205
- return $this->strings->getUrlWithoutScheme( $this->options->cloneHostname );
 
 
 
 
 
 
206
  }
207
 
208
- if( $this->isSubDir() ) {
209
- return trailingslashit( $this->strings->getUrlWithoutScheme( $this->multisiteDomainWithoutScheme ) ) . $this->getSubDir() . '/' . $this->options->cloneDirectoryName;
 
210
  }
211
 
212
- // Get the path to the main multisite without appending and trailingslash e.g. wordpress
213
- $multisitePath = defined( 'PATH_CURRENT_SITE' ) ? PATH_CURRENT_SITE : '/';
214
- $url = rtrim( $this->strings->getUrlWithoutScheme( $this->multisiteDomainWithoutScheme ), '/\\' ) . $multisitePath . $this->options->cloneDirectoryName;
215
- return $url;
 
 
 
 
216
  }
217
 
 
218
  /**
219
  * Get the install sub directory if WP is installed in sub directory
220
  * @return string
221
  */
222
- private function getSubDir() {
223
- $home = get_option( 'home' );
224
- $siteurl = get_option( 'siteurl' );
 
225
 
226
- if( empty( $home ) || empty( $siteurl ) ) {
227
  return '';
228
  }
229
 
230
- $dir = str_replace( $home, '', $siteurl );
231
- return str_replace( '/', '', $dir );
232
  }
233
 
234
  /**
@@ -236,14 +259,15 @@ class SearchReplace extends JobExecutable {
236
  * @param string $new
237
  * @param string $old
238
  */
239
- private function startReplace( $table ) {
 
240
  $rows = $this->options->job->start + $this->settings->querySRLimit;
241
  $this->log(
242
- "DB Search & Replace: Table {$table} {$this->options->job->start} to {$rows} records"
243
  );
244
 
245
  // Search & Replace
246
- $this->searchReplace( $table, $rows, array() );
247
 
248
  // Set new offset
249
  $this->options->job->start += $this->settings->querySRLimit;
@@ -252,17 +276,18 @@ class SearchReplace extends JobExecutable {
252
  /**
253
  * Gets the columns in a table.
254
  * @access public
255
- * @param string $table The table to check.
256
  * @return array
257
  */
258
- private function get_columns( $table ) {
 
259
  $primary_key = null;
260
- $columns = array();
261
- $fields = $this->productionDb->get_results( 'DESCRIBE ' . $table );
262
- if( is_array( $fields ) ) {
263
- foreach ( $fields as $column ) {
264
  $columns[] = $column->Field;
265
- if( $column->Key == 'PRI' ) {
266
  $primary_key = $column->Field;
267
  }
268
  }
@@ -279,189 +304,190 @@ class SearchReplace extends JobExecutable {
279
  * @link https://interconnectit.com/products/search-and-replace-for-wordpress-databases/
280
  *
281
  * @access public
282
- * @param string $table The table to run the replacement on.
283
- * @param int $page The page/block to begin the query on.
284
- * @param array $args An associative array containing arguments for this run.
285
  * @return array
286
  */
287
  private function searchReplace($table, $page, $args)
288
  {
289
 
290
- if ($this->thirdParty->isSearchReplaceExcluded($table)) {
291
- $this->log("DB Search & Replace: Skip {$table}", \WPStaging\Utils\Logger::TYPE_INFO);
292
- return true;
293
- }
294
 
295
  $table = esc_sql($table);
296
 
297
  $args['search_for'] = array(
298
- '%2F%2F'.str_replace('/', '%2F', $this->sourceHostname), // HTML entitity for WP Backery Page Builder Plugin
299
- '\/\/'.str_replace('/', '\/', $this->sourceHostname), // Escaped \/ used by revslider and several visual editors
300
- '//'.$this->sourceHostname, // //example.com
301
  ABSPATH
302
- );
303
 
304
- $args['replace_with'] = array(
305
- '%2F%2F'.str_replace('/', '%2F', $this->destinationHostname),
306
- '\/\/'.str_replace('/', '\/', $this->destinationHostname),
307
- '//'.$this->destinationHostname,
308
  $this->options->destinationDir
309
- );
310
 
311
- $this->debugLog("DB Search & Replace: Search: {$args['search_for'][0]}", \WPStaging\Utils\Logger::TYPE_INFO);
312
- $this->debugLog("DB Search & Replace: Replace: {$args['replace_with'][0]}", \WPStaging\Utils\Logger::TYPE_INFO);
313
 
314
 
315
- $args['replace_guids'] = 'off';
316
- $args['dry_run'] = 'off';
317
- $args['case_insensitive'] = false;
318
- $args['skip_transients'] = 'on';
319
 
320
 
321
- // Allow filtering of search & replace parameters
322
- $args = apply_filters('wpstg_clone_searchreplace_params', $args);
323
 
324
  // Get columns and primary keys
325
- list( $primary_key, $columns ) = $this->get_columns($table);
326
-
327
- // Bail out early if there isn't a primary key.
328
- // We commented this to search & replace through tables which have no primary keys like wp_revslider_slides
329
- // @todo test this carefully. If it causes (performance) issues we need to activate it again!
330
- // @since 2.4.4
331
- // if( null === $primary_key ) {
332
- // return false;
333
- // }
334
-
335
- $current_row = 0;
336
- $start = $this->options->job->start;
337
- $end = $this->settings->querySRLimit;
338
-
339
- $data = $this->productionDb->get_results("SELECT * FROM $table LIMIT $start, $end", ARRAY_A);
340
-
341
- // Filter certain rows option_name in wpstg_options
342
- $filter = array(
343
- 'Admin_custome_login_Slidshow',
344
- 'Admin_custome_login_Social',
345
- 'Admin_custome_login_logo',
346
- 'Admin_custome_login_text',
347
- 'Admin_custome_login_login',
348
- 'Admin_custome_login_top',
349
- 'Admin_custome_login_dashboard',
350
- 'Admin_custome_login_Version',
351
  'upload_path',
352
- 'wpstg_existing_clones_beta',
353
- 'wpstg_existing_clones',
354
- 'wpstg_settings',
355
- 'wpstg_license_status',
356
- 'siteurl',
357
- 'home'
358
- );
359
 
360
- $filter = apply_filters('wpstg_clone_searchreplace_excl_rows', $filter);
361
 
362
  // Go through the table rows
363
- foreach ($data as $row) {
364
- $current_row++;
365
- $update_sql = array();
366
- $where_sql = array();
367
- $upd = false;
368
 
369
  // Skip rows
370
- if (isset($row['option_name']) && in_array($row['option_name'], $filter)) {
371
- continue;
372
- }
373
 
374
  // Skip transients (There can be thousands of them. Save memory and increase performance)
375
- if (isset($row['option_name']) && 'on' === $args['skip_transients'] && false
376
- !== strpos($row['option_name'], '_transient')) {
377
- continue;
378
- }
379
  // Skip rows with more than 5MB to save memory. These rows contain log data or something similiar but never site relevant data
380
- if (isset($row['option_value']) && strlen($row['option_value']) >= 5000000) {
381
- continue;
382
- }
383
 
384
- // Go through the columns
385
- foreach ($columns as $column) {
386
 
387
- $dataRow = $row[$column];
388
 
389
  // Skip column larger than 5MB
390
- $size = strlen($dataRow);
391
- if ($size >= 5000000) {
392
- continue;
393
- }
394
 
395
  // Skip primary key column
396
- if ($column == $primary_key) {
397
- $where_sql[] = $column.' = "'.$this->mysql_escape_mimic($dataRow).'"';
398
- continue;
399
- }
400
 
401
- // Skip GUIDs by default.
402
- if ('on' !== $args['replace_guids'] && 'guid' == $column) {
403
- continue;
404
- }
405
 
406
 
407
- $i = 0;
408
  foreach ($args['search_for'] as $replace) {
409
  $dataRow = $this->recursive_unserialize_replace($args['search_for'][$i], $args['replace_with'][$i], $dataRow, false, $args['case_insensitive']);
410
- $i++;
411
- }
412
  unset($replace, $i);
413
 
414
- // Something was changed
415
- if ($row[$column] != $dataRow) {
416
- $update_sql[] = $column.' = "'.$this->mysql_escape_mimic($dataRow).'"';
417
- $upd = true;
418
- }
419
- }
420
-
421
- // Determine what to do with updates.
422
- if ($args['dry_run'] === 'on') {
423
- // Don't do anything if a dry run
424
- } elseif ($upd && !empty($where_sql)) {
425
- // If there are changes to make, run the query.
426
- $sql = 'UPDATE '.$table.' SET '.implode(', ', $update_sql).' WHERE '.implode(' AND ', array_filter($where_sql));
427
- $result = $this->productionDb->query($sql);
428
-
429
- if (!$result) {
430
- $this->log("Error updating row {$current_row} SQL: {$sql}", \WPStaging\Utils\Logger::TYPE_ERROR);
431
- }
432
- }
433
- } // end row loop
434
- unset($row);
435
- unset($update_sql);
436
- unset($where_sql);
437
- unset($sql);
438
- unset($current_row);
439
-
440
- // DB Flush
441
- $this->productionDb->flush();
442
- return true;
443
  }
444
 
445
  /**
446
  * Get path to multisite image folder e.g. wp-content/blogs.dir/ID/files or wp-content/uploads/sites/ID
447
  * @return string
448
  */
449
- private function getImagePathLive() {
 
450
  // Check first which structure is used
451
  $uploads = wp_upload_dir();
452
  $basedir = $uploads['basedir'];
453
- $blogId = get_current_blog_id();
454
 
455
- if( false === strpos( $basedir, 'blogs.dir' ) ) {
456
  // Since WP 3.5
457
  $path = $blogId > 1 ?
458
- 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'sites' . DIRECTORY_SEPARATOR . get_current_blog_id() . DIRECTORY_SEPARATOR :
459
- 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR;
460
  } else {
461
  // old blog structure
462
  $path = $blogId > 1 ?
463
- 'wp-content' . DIRECTORY_SEPARATOR . 'blogs.dir' . DIRECTORY_SEPARATOR . get_current_blog_id() . DIRECTORY_SEPARATOR . 'files' . DIRECTORY_SEPARATOR :
464
- 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR;
465
  }
466
  return $path;
467
  }
@@ -470,7 +496,8 @@ class SearchReplace extends JobExecutable {
470
  * Get path to staging site image path wp-content/uploads
471
  * @return string
472
  */
473
- private function getImagePathStaging() {
 
474
  return 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR;
475
  }
476
 
@@ -483,68 +510,69 @@ class SearchReplace extends JobExecutable {
483
  * unserialising any subordinate arrays and performing the replace on those too.
484
  *
485
  * @access private
486
- * @param string $from String we're looking to replace.
487
- * @param string $to What we want it to be replaced with
488
- * @param array $data Used to pass any subordinate arrays back to in.
489
- * @param boolean $serialized Does the array passed via $data need serialising.
490
- * @param sting|boolean $case_insensitive Set to 'on' if we should ignore case, false otherwise.
491
  *
492
- * @return string|array The original array with all elements replaced as needed.
493
  */
494
- private function recursive_unserialize_replace( $from = '', $to = '', $data = '', $serialized = false, $case_insensitive = false ) {
 
495
  try {
496
  // PDO instances can not be serialized or unserialized
497
- if( is_serialized( $data ) && strpos( $data, 'O:3:"PDO":0:' ) !== false ) {
498
  return $data;
499
  }
500
  // DateTime object can not be unserialized.
501
  // Would throw PHP Fatal error: Uncaught Error: Invalid serialization data for DateTime object in
502
  // Bug PHP https://bugs.php.net/bug.php?id=68889&thanks=6 and https://github.com/WP-Staging/wp-staging-pro/issues/74
503
- if( is_serialized( $data ) && strpos( $data, 'O:8:"DateTime":0:' ) !== false ) {
504
  return $data;
505
  }
506
  // Some unserialized data cannot be re-serialized eg. SimpleXMLElements
507
- if( is_serialized( $data ) && ( $unserialized = @unserialize( $data ) ) !== false ) {
508
- $data = $this->recursive_unserialize_replace( $from, $to, $unserialized, true, $case_insensitive );
509
- } elseif( is_array( $data ) ) {
510
  $tmp = array();
511
- foreach ( $data as $key => $value ) {
512
- $tmp[$key] = $this->recursive_unserialize_replace( $from, $to, $value, false, $case_insensitive );
513
  }
514
 
515
  $data = $tmp;
516
- unset( $tmp );
517
- } elseif( is_object( $data ) ) {
518
- $props = get_object_vars( $data );
519
 
520
  // Do a search & replace
521
- if( empty( $props['__PHP_Incomplete_Class_Name'] ) ) {
522
  $tmp = $data;
523
- foreach ( $props as $key => $value ) {
524
- if( $key === '' || ord( $key[0] ) === 0 ) {
525
  continue;
526
  }
527
- $tmp->$key = $this->recursive_unserialize_replace( $from, $to, $value, false, $case_insensitive );
528
  }
529
- $data = $tmp;
530
- $tmp = '';
531
  $props = '';
532
- unset( $tmp );
533
- unset( $props );
534
  }
535
  } else {
536
- if( is_string( $data ) ) {
537
- if( !empty( $from ) && !empty( $to ) ) {
538
- $data = $this->str_replace( $from, $to, $data, $case_insensitive );
539
  }
540
  }
541
  }
542
 
543
- if( $serialized ) {
544
- return serialize( $data );
545
  }
546
- } catch ( Exception $error ) {
547
-
548
  }
549
 
550
  return $data;
@@ -554,15 +582,16 @@ class SearchReplace extends JobExecutable {
554
  * Mimics the mysql_real_escape_string function. Adapted from a post by 'feedr' on php.net.
555
  * @link http://php.net/manual/en/function.mysql-real-escape-string.php#101248
556
  * @access public
557
- * @param string $input The string to escape.
558
  * @return string
559
  */
560
- private function mysql_escape_mimic( $input ) {
561
- if( is_array( $input ) ) {
562
- return array_map( __METHOD__, $input );
 
563
  }
564
- if( !empty( $input ) && is_string( $input ) ) {
565
- return str_replace( array('\\', "\0", "\n", "\r", "'", '"', "\x1a"), array('\\\\', '\\0', '\\n', '\\r', "\\'", '\\"', '\\Z'), $input );
566
  }
567
 
568
  return $input;
@@ -572,17 +601,18 @@ class SearchReplace extends JobExecutable {
572
  * Return unserialized object or array
573
  *
574
  * @param string $serialized_string Serialized string.
575
- * @param string $method The name of the caller method.
576
  *
577
  * @return mixed, false on failure
578
  */
579
- private static function unserialize( $serialized_string ) {
580
- if( !is_serialized( $serialized_string ) ) {
 
581
  return false;
582
  }
583
 
584
- $serialized_string = trim( $serialized_string );
585
- $unserialized_string = @unserialize( $serialized_string );
586
 
587
  return $unserialized_string;
588
  }
@@ -597,23 +627,24 @@ class SearchReplace extends JobExecutable {
597
  *
598
  * @return string
599
  */
600
- private function str_replace( $from, $to, $data, $case_insensitive = false ) {
 
601
 
602
  // Add filter
603
- $excludes = apply_filters( 'wpstg_clone_searchreplace_excl', array() );
604
 
605
  // Build pattern
606
  $regexExclude = '';
607
- foreach ( $excludes as $exclude ) {
608
  $regexExclude .= $exclude . '(*SKIP)(FAIL)|';
609
  }
610
 
611
- if( 'on' === $case_insensitive ) {
612
  //$data = str_ireplace( $from, $to, $data );
613
- $data = preg_replace( '#' . $regexExclude . preg_quote( $from ) . '#i', $to, $data );
614
  } else {
615
  //$data = str_replace( $from, $to, $data );
616
- $data = preg_replace( '#' . $regexExclude . preg_quote( $from ) . '#', $to, $data );
617
  }
618
 
619
  return $data;
@@ -623,13 +654,14 @@ class SearchReplace extends JobExecutable {
623
  * Set the job
624
  * @param string $table
625
  */
626
- private function setJob( $table ) {
627
- if( !empty( $this->options->job->current ) ) {
 
628
  return;
629
  }
630
 
631
  $this->options->job->current = $table;
632
- $this->options->job->start = 0;
633
  }
634
 
635
  /**
@@ -638,21 +670,22 @@ class SearchReplace extends JobExecutable {
638
  * @param string $old
639
  * @return bool
640
  */
641
- private function startJob( $new, $old ) {
 
642
 
643
- if( $this->isExcludedTable( $new ) ) {
644
  return false;
645
  }
646
 
647
  // Table does not exist
648
- $result = $this->productionDb->query( "SHOW TABLES LIKE '{$old}'" );
649
- if( !$result || 0 === $result ) {
650
  return false;
651
  }
652
 
653
- $this->options->job->total = ( int ) $this->productionDb->get_var( "SELECT COUNT(1) FROM {$old}" );
654
 
655
- if( 0 == $this->options->job->total ) {
656
  $this->finishStep();
657
  return false;
658
  }
@@ -665,19 +698,20 @@ class SearchReplace extends JobExecutable {
665
  * @param string $table
666
  * @return boolean
667
  */
668
- private function isExcludedTable( $table ) {
 
669
 
670
- $customTables = apply_filters( 'wpstg_clone_searchreplace_tables_exclude', array() );
671
  $defaultTables = array('blogs');
672
 
673
- $tables = array_merge( $customTables, $defaultTables );
674
 
675
  $excludedTables = array();
676
- foreach ( $tables as $key => $value ) {
677
  $excludedTables[] = $this->options->prefix . $value;
678
  }
679
 
680
- if( in_array( $table, $excludedTables ) ) {
681
  return true;
682
  }
683
  return false;
@@ -686,9 +720,10 @@ class SearchReplace extends JobExecutable {
686
  /**
687
  * Finish the step
688
  */
689
- private function finishStep() {
 
690
  // This job is not finished yet
691
- if( $this->options->job->total > $this->options->job->start ) {
692
  return false;
693
  }
694
 
@@ -705,15 +740,16 @@ class SearchReplace extends JobExecutable {
705
  * Drop table if necessary
706
  * @param string $new
707
  */
708
- private function dropTable( $new ) {
709
- $old = $this->productionDb->get_var( $this->productionDb->prepare( "SHOW TABLES LIKE %s", $new ) );
 
710
 
711
- if( !$this->shouldDropTable( $new, $old ) ) {
712
  return;
713
  }
714
 
715
- $this->log( "DB Search & Replace: {$new} already exists, dropping it first" );
716
- $this->productionDb->query( "DROP TABLE {$new}" );
717
  }
718
 
719
  /**
@@ -722,28 +758,30 @@ class SearchReplace extends JobExecutable {
722
  * @param string $old
723
  * @return bool
724
  */
725
- private function shouldDropTable( $new, $old ) {
 
726
  return (
727
- $old == $new &&
728
- (
729
- !isset( $this->options->job->current ) ||
730
- !isset( $this->options->job->start ) ||
731
  0 == $this->options->job->start
732
- )
733
- );
734
  }
735
 
736
  /**
737
  * Check if WP is installed in subdir
738
  * @return boolean
739
  */
740
- private function isSubDir() {
 
741
  // Compare names without scheme to bypass cases where siteurl and home have different schemes http / https
742
  // This is happening much more often than you would expect
743
- $siteurl = preg_replace( '#^https?://#', '', rtrim( get_option( 'siteurl' ), '/' ) );
744
- $home = preg_replace( '#^https?://#', '', rtrim( get_option( 'home' ), '/' ) );
745
 
746
- if( $home !== $siteurl ) {
747
  return true;
748
  }
749
  return false;
3
  namespace WPStaging\Backend\Modules\Jobs\Multisite;
4
 
5
  // No Direct Access
6
+ if (!defined("WPINC")) {
7
  die;
8
  }
9
 
17
  * Class Database
18
  * @package WPStaging\Backend\Modules\Jobs
19
  */
20
+ class SearchReplace extends JobExecutable
21
+ {
22
 
23
  /**
24
  * @var int
25
  */
26
  private $total = 0;
27
+
28
  /**
29
  * @var \WPDB
30
  */
36
  */
37
  private $stagingDb;
38
 
39
+ /**
40
  *
41
  * @var string
42
  */
56
 
57
  /**
58
  * The prefix of the new database tables which are used for the live site after updating tables
59
+ * @var string
60
  */
61
  public $tmpPrefix;
62
 
63
+ /**
64
  * Initialize
65
  */
66
+ public function initialize()
67
+ {
68
+ $this->total = count($this->options->tables);
69
+ $this->stagingDb = $this->getStagingDB();
70
+ $this->productionDb = WPStaging::getInstance()->get("wpdb");
71
+ $this->tmpPrefix = $this->options->prefix;
72
+ $this->strings = new Strings();
73
+ $this->sourceHostname = $this->getSourceHostname();
74
  $this->destinationHostname = $this->getDestinationHostname();
75
  }
76
 
77
  /**
78
  * Get database object to interact with
79
  */
80
+ private function getStagingDB()
81
+ {
82
+ if (empty($this->options->databaseUser) || empty($this->options->databasePassword) || empty($this->options->databaseDatabase) || empty($this->options->databaseServer)) {
83
+ return null;
84
+ }
85
+ return new \wpdb($this->options->databaseUser, $this->options->databasePassword, $this->options->databaseDatabase, $this->options->databaseServer);
86
  }
87
 
88
+ public function start()
89
+ {
90
  // Skip job. Nothing to do
91
+ if ($this->options->totalSteps === 0) {
92
+ $this->prepareResponse(true, false);
93
  }
94
 
95
  $this->run();
97
  // Save option, progress
98
  $this->saveOptions();
99
 
100
+ return ( object )$this->response;
101
  }
102
 
103
  /**
104
  * Calculate Total Steps in This Job and Assign It to $this->options->totalSteps
105
  * @return void
106
  */
107
+ protected function calculateTotalSteps()
108
+ {
109
  $this->options->totalSteps = $this->total;
110
  }
111
 
114
  * Returns false when over threshold limits are hit or when the job is done, true otherwise
115
  * @return bool
116
  */
117
+ protected function execute()
118
+ {
119
  // Over limits threshold
120
+ if ($this->isOverThreshold()) {
121
  // Prepare response and save current progress
122
+ $this->prepareResponse(false, false);
123
  $this->saveOptions();
124
  return false;
125
  }
126
 
127
  // No more steps, finished
128
+ if ($this->options->currentStep > $this->total || !isset($this->options->tables[$this->options->currentStep])) {
129
+ $this->prepareResponse(true, false);
130
  return false;
131
  }
132
 
133
  // Table is excluded
134
+ if (in_array($this->options->tables[$this->options->currentStep], $this->options->excludedTables)) {
135
  $this->prepareResponse();
136
  return true;
137
  }
138
 
139
  // Search & Replace
140
+ if (!$this->stopExecution() && !$this->updateTable($this->options->tables[$this->options->currentStep])) {
141
  // Prepare Response
142
+ $this->prepareResponse(false, false);
143
 
144
  // Not finished
145
  return true;
157
  * Stop Execution immediately
158
  * return mixed bool | json
159
  */
160
+ private function stopExecution()
161
+ {
162
+ if ($this->productionDb->prefix == $this->tmpPrefix) {
163
+ $this->returnException('Fatal Error 9: Prefix ' . $this->productionDb->prefix . ' is used for the live site hence it can not be used for the staging site as well. Please ask support@wp-staging.com how to resolve this.');
164
  }
165
  return false;
166
  }
170
  * @param string $tableName
171
  * @return bool
172
  */
173
+ private function updateTable($tableName)
174
+ {
175
+ $strings = new Strings();
176
+ $table = $strings->str_replace_first($this->productionDb->prefix, '', $tableName);
177
  $newTableName = $this->tmpPrefix . $table;
178
 
179
  // Save current job
180
+ $this->setJob($newTableName);
181
 
182
  // Beginning of the job
183
+ if (!$this->startJob($newTableName, $tableName)) {
184
  return true;
185
  }
186
  // Copy data
187
+ $this->startReplace($newTableName);
188
 
189
  // Finish the step
190
  return $this->finishStep();
194
  * Get source Hostname depending on wheather WP has been installed in sub dir or not
195
  * @return type
196
  */
197
+ private function getSourceHostname()
198
+ {
199
 
200
+ if ($this->isSubDir()) {
201
+ return trailingslashit($this->multisiteHomeUrlWithoutScheme) . '/' . $this->getSubDir();
202
  }
203
  return $this->multisiteHomeUrlWithoutScheme;
204
  }
205
 
206
  /**
207
+ * Get destination Hostname depending on WP installed in sub dir or not
208
+ * Return host name without scheme
209
+ * @return string
210
  */
211
+ private function getDestinationHostname()
212
+ {
213
 
214
+ // Staging site is updated so do not change hostname
215
+ if ($this->options->mainJob === 'updating') {
216
+ // If target hostname is defined in advanced settings prefer its use (pro only)
217
+ if (!empty($this->options->cloneHostname)) {
218
+ return $this->strings->getUrlWithoutScheme($this->options->cloneHostname);
219
+ } else {
220
+ return $this->strings->getUrlWithoutScheme($this->options->destinationHostname);
221
+ }
222
  }
223
 
224
+ // Target hostname defined in advanced settings (pro only)
225
+ if (!empty($this->options->cloneHostname)) {
226
+ return $this->strings->getUrlWithoutScheme($this->options->cloneHostname);
227
  }
228
 
229
+ // WP installed in sub directory under root
230
+ if ($this->isSubDir()) {
231
+ return trailingslashit($this->strings->getUrlWithoutScheme(get_home_url())) . $this->getSubDir() . '/' . $this->options->cloneDirectoryName;
232
+ }
233
+
234
+ // Path to root of main multisite without leading or trailing slash e.g.: wordpress
235
+ $multisitePath = defined('PATH_CURRENT_SITE') ? PATH_CURRENT_SITE : '/';
236
+ return rtrim($this->strings->getUrlWithoutScheme(get_home_url()), '/\\') . $multisitePath . $this->options->cloneDirectoryName;
237
  }
238
 
239
+
240
  /**
241
  * Get the install sub directory if WP is installed in sub directory
242
  * @return string
243
  */
244
+ private function getSubDir()
245
+ {
246
+ $home = get_option('home');
247
+ $siteurl = get_option('siteurl');
248
 
249
+ if (empty($home) || empty($siteurl)) {
250
  return '';
251
  }
252
 
253
+ $dir = str_replace($home, '', $siteurl);
254
+ return str_replace('/', '', $dir);
255
  }
256
 
257
  /**
259
  * @param string $new
260
  * @param string $old
261
  */
262
+ private function startReplace($table)
263
+ {
264
  $rows = $this->options->job->start + $this->settings->querySRLimit;
265
  $this->log(
266
+ "DB Search & Replace: Table {$table} {$this->options->job->start} to {$rows} records"
267
  );
268
 
269
  // Search & Replace
270
+ $this->searchReplace($table, $rows, array());
271
 
272
  // Set new offset
273
  $this->options->job->start += $this->settings->querySRLimit;
276
  /**
277
  * Gets the columns in a table.
278
  * @access public
279
+ * @param string $table The table to check.
280
  * @return array
281
  */
282
+ private function get_columns($table)
283
+ {
284
  $primary_key = null;
285
+ $columns = array();
286
+ $fields = $this->productionDb->get_results('DESCRIBE ' . $table);
287
+ if (is_array($fields)) {
288
+ foreach ($fields as $column) {
289
  $columns[] = $column->Field;
290
+ if ($column->Key == 'PRI') {
291
  $primary_key = $column->Field;
292
  }
293
  }
304
  * @link https://interconnectit.com/products/search-and-replace-for-wordpress-databases/
305
  *
306
  * @access public
307
+ * @param string $table The table to run the replacement on.
308
+ * @param int $page The page/block to begin the query on.
309
+ * @param array $args An associative array containing arguments for this run.
310
  * @return array
311
  */
312
  private function searchReplace($table, $page, $args)
313
  {
314
 
315
+ if ($this->thirdParty->isSearchReplaceExcluded($table)) {
316
+ $this->log("DB Search & Replace: Skip {$table}", \WPStaging\Utils\Logger::TYPE_INFO);
317
+ return true;
318
+ }
319
 
320
  $table = esc_sql($table);
321
 
322
  $args['search_for'] = array(
323
+ '%2F%2F' . str_replace('/', '%2F', $this->sourceHostname), // HTML entitity for WP Backery Page Builder Plugin
324
+ '\/\/' . str_replace('/', '\/', $this->sourceHostname), // Escaped \/ used by revslider and several visual editors
325
+ '//' . $this->sourceHostname, // //example.com
326
  ABSPATH
327
+ );
328
 
329
+ $args['replace_with'] = array(
330
+ '%2F%2F' . str_replace('/', '%2F', $this->destinationHostname),
331
+ '\/\/' . str_replace('/', '\/', $this->destinationHostname),
332
+ '//' . $this->destinationHostname,
333
  $this->options->destinationDir
334
+ );
335
 
336
+ $this->debugLog("DB Search & Replace: Search: {$args['search_for'][0]}", \WPStaging\Utils\Logger::TYPE_INFO);
337
+ $this->debugLog("DB Search & Replace: Replace: {$args['replace_with'][0]}", \WPStaging\Utils\Logger::TYPE_INFO);
338
 
339
 
340
+ $args['replace_guids'] = 'off';
341
+ $args['dry_run'] = 'off';
342
+ $args['case_insensitive'] = false;
343
+ $args['skip_transients'] = 'on';
344
 
345
 
346
+ // Allow filtering of search & replace parameters
347
+ $args = apply_filters('wpstg_clone_searchreplace_params', $args);
348
 
349
  // Get columns and primary keys
350
+ list($primary_key, $columns) = $this->get_columns($table);
351
+
352
+ // Bail out early if there isn't a primary key.
353
+ // We commented this to search & replace through tables which have no primary keys like wp_revslider_slides
354
+ // @todo test this carefully. If it causes (performance) issues we need to activate it again!
355
+ // @since 2.4.4
356
+ // if( null === $primary_key ) {
357
+ // return false;
358
+ // }
359
+
360
+ $current_row = 0;
361
+ $start = $this->options->job->start;
362
+ $end = $this->settings->querySRLimit;
363
+
364
+ $data = $this->productionDb->get_results("SELECT * FROM $table LIMIT $start, $end", ARRAY_A);
365
+
366
+ // Filter certain rows option_name in wpstg_options
367
+ $filter = array(
368
+ 'Admin_custome_login_Slidshow',
369
+ 'Admin_custome_login_Social',
370
+ 'Admin_custome_login_logo',
371
+ 'Admin_custome_login_text',
372
+ 'Admin_custome_login_login',
373
+ 'Admin_custome_login_top',
374
+ 'Admin_custome_login_dashboard',
375
+ 'Admin_custome_login_Version',
376
  'upload_path',
377
+ 'wpstg_existing_clones_beta',
378
+ 'wpstg_existing_clones',
379
+ 'wpstg_settings',
380
+ 'wpstg_license_status',
381
+ 'siteurl',
382
+ 'home'
383
+ );
384
 
385
+ $filter = apply_filters('wpstg_clone_searchreplace_excl_rows', $filter);
386
 
387
  // Go through the table rows
388
+ foreach ($data as $row) {
389
+ $current_row++;
390
+ $update_sql = array();
391
+ $where_sql = array();
392
+ $upd = false;
393
 
394
  // Skip rows
395
+ if (isset($row['option_name']) && in_array($row['option_name'], $filter)) {
396
+ continue;
397
+ }
398
 
399
  // Skip transients (There can be thousands of them. Save memory and increase performance)
400
+ if (isset($row['option_name']) && 'on' === $args['skip_transients'] && false
401
+ !== strpos($row['option_name'], '_transient')) {
402
+ continue;
403
+ }
404
  // Skip rows with more than 5MB to save memory. These rows contain log data or something similiar but never site relevant data
405
+ if (isset($row['option_value']) && strlen($row['option_value']) >= 5000000) {
406
+ continue;
407
+ }
408
 
409
+ // Go through the columns
410
+ foreach ($columns as $column) {
411
 
412
+ $dataRow = $row[$column];
413
 
414
  // Skip column larger than 5MB
415
+ $size = strlen($dataRow);
416
+ if ($size >= 5000000) {
417
+ continue;
418
+ }
419
 
420
  // Skip primary key column
421
+ if ($column == $primary_key) {
422
+ $where_sql[] = $column . ' = "' . $this->mysql_escape_mimic($dataRow) . '"';
423
+ continue;
424
+ }
425
 
426
+ // Skip GUIDs by default.
427
+ if ('on' !== $args['replace_guids'] && 'guid' == $column) {
428
+ continue;
429
+ }
430
 
431
 
432
+ $i = 0;
433
  foreach ($args['search_for'] as $replace) {
434
  $dataRow = $this->recursive_unserialize_replace($args['search_for'][$i], $args['replace_with'][$i], $dataRow, false, $args['case_insensitive']);
435
+ $i++;
436
+ }
437
  unset($replace, $i);
438
 
439
+ // Something was changed
440
+ if ($row[$column] != $dataRow) {
441
+ $update_sql[] = $column . ' = "' . $this->mysql_escape_mimic($dataRow) . '"';
442
+ $upd = true;
443
+ }
444
+ }
445
+
446
+ // Determine what to do with updates.
447
+ if ($args['dry_run'] === 'on') {
448
+ // Don't do anything if a dry run
449
+ } elseif ($upd && !empty($where_sql)) {
450
+ // If there are changes to make, run the query.
451
+ $sql = 'UPDATE ' . $table . ' SET ' . implode(', ', $update_sql) . ' WHERE ' . implode(' AND ', array_filter($where_sql));
452
+ $result = $this->productionDb->query($sql);
453
+
454
+ if (!$result) {
455
+ $this->log("Error updating row {$current_row} SQL: {$sql}", \WPStaging\Utils\Logger::TYPE_ERROR);
456
+ }
457
+ }
458
+ } // end row loop
459
+ unset($row);
460
+ unset($update_sql);
461
+ unset($where_sql);
462
+ unset($sql);
463
+ unset($current_row);
464
+
465
+ // DB Flush
466
+ $this->productionDb->flush();
467
+ return true;
468
  }
469
 
470
  /**
471
  * Get path to multisite image folder e.g. wp-content/blogs.dir/ID/files or wp-content/uploads/sites/ID
472
  * @return string
473
  */
474
+ private function getImagePathLive()
475
+ {
476
  // Check first which structure is used
477
  $uploads = wp_upload_dir();
478
  $basedir = $uploads['basedir'];
479
+ $blogId = get_current_blog_id();
480
 
481
+ if (false === strpos($basedir, 'blogs.dir')) {
482
  // Since WP 3.5
483
  $path = $blogId > 1 ?
484
+ 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'sites' . DIRECTORY_SEPARATOR . get_current_blog_id() . DIRECTORY_SEPARATOR :
485
+ 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR;
486
  } else {
487
  // old blog structure
488
  $path = $blogId > 1 ?
489
+ 'wp-content' . DIRECTORY_SEPARATOR . 'blogs.dir' . DIRECTORY_SEPARATOR . get_current_blog_id() . DIRECTORY_SEPARATOR . 'files' . DIRECTORY_SEPARATOR :
490
+ 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR;
491
  }
492
  return $path;
493
  }
496
  * Get path to staging site image path wp-content/uploads
497
  * @return string
498
  */
499
+ private function getImagePathStaging()
500
+ {
501
  return 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR;
502
  }
503
 
510
  * unserialising any subordinate arrays and performing the replace on those too.
511
  *
512
  * @access private
513
+ * @param string $from String we're looking to replace.
514
+ * @param string $to What we want it to be replaced with
515
+ * @param array $data Used to pass any subordinate arrays back to in.
516
+ * @param boolean $serialized Does the array passed via $data need serialising.
517
+ * @param sting|boolean $case_insensitive Set to 'on' if we should ignore case, false otherwise.
518
  *
519
+ * @return string|array The original array with all elements replaced as needed.
520
  */
521
+ private function recursive_unserialize_replace($from = '', $to = '', $data = '', $serialized = false, $case_insensitive = false)
522
+ {
523
  try {
524
  // PDO instances can not be serialized or unserialized
525
+ if (is_serialized($data) && strpos($data, 'O:3:"PDO":0:') !== false) {
526
  return $data;
527
  }
528
  // DateTime object can not be unserialized.
529
  // Would throw PHP Fatal error: Uncaught Error: Invalid serialization data for DateTime object in
530
  // Bug PHP https://bugs.php.net/bug.php?id=68889&thanks=6 and https://github.com/WP-Staging/wp-staging-pro/issues/74
531
+ if (is_serialized($data) && strpos($data, 'O:8:"DateTime":0:') !== false) {
532
  return $data;
533
  }
534
  // Some unserialized data cannot be re-serialized eg. SimpleXMLElements
535
+ if (is_serialized($data) && ($unserialized = @unserialize($data)) !== false) {
536
+ $data = $this->recursive_unserialize_replace($from, $to, $unserialized, true, $case_insensitive);
537
+ } elseif (is_array($data)) {
538
  $tmp = array();
539
+ foreach ($data as $key => $value) {
540
+ $tmp[$key] = $this->recursive_unserialize_replace($from, $to, $value, false, $case_insensitive);
541
  }
542
 
543
  $data = $tmp;
544
+ unset($tmp);
545
+ } elseif (is_object($data)) {
546
+ $props = get_object_vars($data);
547
 
548
  // Do a search & replace
549
+ if (empty($props['__PHP_Incomplete_Class_Name'])) {
550
  $tmp = $data;
551
+ foreach ($props as $key => $value) {
552
+ if ($key === '' || ord($key[0]) === 0) {
553
  continue;
554
  }
555
+ $tmp->$key = $this->recursive_unserialize_replace($from, $to, $value, false, $case_insensitive);
556
  }
557
+ $data = $tmp;
558
+ $tmp = '';
559
  $props = '';
560
+ unset($tmp);
561
+ unset($props);
562
  }
563
  } else {
564
+ if (is_string($data)) {
565
+ if (!empty($from) && !empty($to)) {
566
+ $data = $this->str_replace($from, $to, $data, $case_insensitive);
567
  }
568
  }
569
  }
570
 
571
+ if ($serialized) {
572
+ return serialize($data);
573
  }
574
+ } catch (Exception $error) {
575
+
576
  }
577
 
578
  return $data;
582
  * Mimics the mysql_real_escape_string function. Adapted from a post by 'feedr' on php.net.
583
  * @link http://php.net/manual/en/function.mysql-real-escape-string.php#101248
584
  * @access public
585
+ * @param string $input The string to escape.
586
  * @return string
587
  */
588
+ private function mysql_escape_mimic($input)
589
+ {
590
+ if (is_array($input)) {
591
+ return array_map(__METHOD__, $input);
592
  }
593
+ if (!empty($input) && is_string($input)) {
594
+ return str_replace(array('\\', "\0", "\n", "\r", "'", '"', "\x1a"), array('\\\\', '\\0', '\\n', '\\r', "\\'", '\\"', '\\Z'), $input);
595
  }
596
 
597
  return $input;
601
  * Return unserialized object or array
602
  *
603
  * @param string $serialized_string Serialized string.
604
+ * @param string $method The name of the caller method.
605
  *
606
  * @return mixed, false on failure
607
  */
608
+ private static function unserialize($serialized_string)
609
+ {
610
+ if (!is_serialized($serialized_string)) {
611
  return false;
612
  }
613
 
614
+ $serialized_string = trim($serialized_string);
615
+ $unserialized_string = @unserialize($serialized_string);
616
 
617
  return $unserialized_string;
618
  }
627
  *
628
  * @return string
629
  */
630
+ private function str_replace($from, $to, $data, $case_insensitive = false)
631
+ {
632
 
633
  // Add filter
634
+ $excludes = apply_filters('wpstg_clone_searchreplace_excl', array());
635
 
636
  // Build pattern
637
  $regexExclude = '';
638
+ foreach ($excludes as $exclude) {
639
  $regexExclude .= $exclude . '(*SKIP)(FAIL)|';
640
  }
641
 
642
+ if ('on' === $case_insensitive) {
643
  //$data = str_ireplace( $from, $to, $data );
644
+ $data = preg_replace('#' . $regexExclude . preg_quote($from) . '#i', $to, $data);
645
  } else {
646
  //$data = str_replace( $from, $to, $data );
647
+ $data = preg_replace('#' . $regexExclude . preg_quote($from) . '#', $to, $data);
648
  }
649
 
650
  return $data;
654
  * Set the job
655
  * @param string $table
656
  */
657
+ private function setJob($table)
658
+ {
659
+ if (!empty($this->options->job->current)) {
660
  return;
661
  }
662
 
663
  $this->options->job->current = $table;
664
+ $this->options->job->start = 0;
665
  }
666
 
667
  /**
670
  * @param string $old
671
  * @return bool
672
  */
673
+ private function startJob($new, $old)
674
+ {
675
 
676
+ if ($this->isExcludedTable($new)) {
677
  return false;
678
  }
679
 
680
  // Table does not exist
681
+ $result = $this->productionDb->query("SHOW TABLES LIKE '{$old}'");
682
+ if (!$result || 0 === $result) {
683
  return false;
684
  }
685
 
686
+ $this->options->job->total = ( int )$this->productionDb->get_var("SELECT COUNT(1) FROM {$old}");
687
 
688
+ if (0 == $this->options->job->total) {
689
  $this->finishStep();
690
  return false;
691
  }
698
  * @param string $table
699
  * @return boolean
700
  */
701
+ private function isExcludedTable($table)
702
+ {
703
 
704
+ $customTables = apply_filters('wpstg_clone_searchreplace_tables_exclude', array());
705
  $defaultTables = array('blogs');
706
 
707
+ $tables = array_merge($customTables, $defaultTables);
708
 
709
  $excludedTables = array();
710
+ foreach ($tables as $key => $value) {
711
  $excludedTables[] = $this->options->prefix . $value;
712
  }
713
 
714
+ if (in_array($table, $excludedTables)) {
715
  return true;
716
  }
717
  return false;
720
  /**
721
  * Finish the step
722
  */
723
+ private function finishStep()
724
+ {
725
  // This job is not finished yet
726
+ if ($this->options->job->total > $this->options->job->start) {
727
  return false;
728
  }
729
 
740
  * Drop table if necessary
741
  * @param string $new
742
  */
743
+ private function dropTable($new)
744
+ {
745
+ $old = $this->productionDb->get_var($this->productionDb->prepare("SHOW TABLES LIKE %s", $new));
746
 
747
+ if (!$this->shouldDropTable($new, $old)) {
748
  return;
749
  }
750
 
751
+ $this->log("DB Search & Replace: {$new} already exists, dropping it first");
752
+ $this->productionDb->query("DROP TABLE {$new}");
753
  }
754
 
755
  /**
758
  * @param string $old
759
  * @return bool
760
  */
761
+ private function shouldDropTable($new, $old)
762
+ {
763
  return (
764
+ $old == $new &&
765
+ (
766
+ !isset($this->options->job->current) ||
767
+ !isset($this->options->job->start) ||
768
  0 == $this->options->job->start
769
+ )
770
+ );
771
  }
772
 
773
  /**
774
  * Check if WP is installed in subdir
775
  * @return boolean
776
  */
777
+ private function isSubDir()
778
+ {
779
  // Compare names without scheme to bypass cases where siteurl and home have different schemes http / https
780
  // This is happening much more often than you would expect
781
+ $siteurl = preg_replace('#^https?://#', '', rtrim(get_option('siteurl'), '/'));
782
+ $home = preg_replace('#^https?://#', '', rtrim(get_option('home'), '/'));
783
 
784
+ if ($home !== $siteurl) {
785
  return true;
786
  }
787
  return false;
Backend/Modules/Jobs/Multisite/SearchReplaceExternal.php CHANGED
@@ -3,7 +3,7 @@
3
  namespace WPStaging\Backend\Modules\Jobs\Multisite;
4
 
5
  // No Direct Access
6
- if( !defined( "WPINC" ) ) {
7
  die;
8
  }
9
 
@@ -15,7 +15,8 @@ use WPStaging\Backend\Modules\Jobs\JobExecutable;
15
  * Class Database
16
  * @package WPStaging\Backend\Modules\Jobs
17
  */
18
- class SearchReplaceExternal extends JobExecutable {
 
19
 
20
  /**
21
  * @var int
@@ -54,34 +55,37 @@ class SearchReplaceExternal extends JobExecutable {
54
 
55
  /**
56
  * The prefix of the new database tables which are used for the live site after updating tables
57
- * @var string
58
  */
59
  public $tmpPrefix;
60
 
61
  /**
62
  * Initialize
63
  */
64
- public function initialize() {
65
- $this->total = count( $this->options->tables );
66
- $this->stagingDb = $this->getStagingDB();
67
- $this->productionDb = WPStaging::getInstance()->get( "wpdb" );
68
- $this->tmpPrefix = $this->options->prefix;
69
- $this->strings = new Strings();
70
- $this->sourceHostname = $this->getSourceHostname();
 
71
  $this->destinationHostname = $this->getDestinationHostname();
72
  }
73
 
74
  /**
75
  * Get database object to interact with
76
  */
77
- private function getStagingDB() {
78
- return new \wpdb( $this->options->databaseUser, $this->options->databasePassword, $this->options->databaseDatabase, $this->options->databaseServer );
 
79
  }
80
 
81
- public function start() {
 
82
  // Skip job. Nothing to do
83
- if( $this->options->totalSteps === 0 ) {
84
- $this->prepareResponse( true, false );
85
  }
86
 
87
  $this->run();
@@ -89,14 +93,15 @@ class SearchReplaceExternal extends JobExecutable {
89
  // Save option, progress
90
  $this->saveOptions();
91
 
92
- return ( object ) $this->response;
93
  }
94
 
95
  /**
96
  * Calculate Total Steps in This Job and Assign It to $this->options->totalSteps
97
  * @return void
98
  */
99
- protected function calculateTotalSteps() {
 
100
  $this->options->totalSteps = $this->total;
101
  }
102
 
@@ -105,38 +110,39 @@ class SearchReplaceExternal extends JobExecutable {
105
  * Returns false when over threshold limits are hit or when the job is done, true otherwise
106
  * @return bool
107
  */
108
- protected function execute() {
 
109
  // Over limits threshold
110
- if( $this->isOverThreshold() ) {
111
  // Prepare response and save current progress
112
- $this->prepareResponse( false, false );
113
  $this->saveOptions();
114
  return false;
115
  }
116
 
117
  // No more steps, finished
118
- if( $this->options->currentStep > $this->total || !isset( $this->options->tables[$this->options->currentStep] ) ) {
119
- $this->prepareResponse( true, false );
120
  return false;
121
  }
122
 
123
  // Table is excluded
124
- if( in_array( $this->options->tables[$this->options->currentStep], $this->options->excludedTables ) ) {
125
  $this->prepareResponse();
126
  return true;
127
  }
128
 
129
  // Search & Replace
130
  if (!$this->updateTable($this->options->tables[$this->options->currentStep])) {
131
- // Prepare Response
132
- $this->prepareResponse(false, false);
133
 
134
- // Not finished
135
- return true;
136
- }
137
 
138
 
139
- // Prepare Response
140
  $this->prepareResponse();
141
 
142
  // Not finished
@@ -148,20 +154,21 @@ class SearchReplaceExternal extends JobExecutable {
148
  * @param string $tableName
149
  * @return bool
150
  */
151
- private function updateTable( $tableName ) {
152
- $strings = new Strings();
153
- $table = $strings->str_replace_first( $this->productionDb->prefix, '', $tableName );
 
154
  $newTableName = $this->tmpPrefix . $table;
155
 
156
  // Save current job
157
- $this->setJob( $newTableName );
158
 
159
  // Beginning of the job
160
- if( !$this->startJob( $newTableName, $tableName ) ) {
161
  return true;
162
  }
163
  // Copy data
164
- $this->startReplace( $newTableName );
165
 
166
  // Finish the step
167
  return $this->finishStep();
@@ -171,49 +178,64 @@ class SearchReplaceExternal extends JobExecutable {
171
  * Get source Hostname depending on wheather WP has been installed in sub dir or not
172
  * @return type
173
  */
174
- private function getSourceHostname() {
 
175
 
176
- if( $this->isSubDir() ) {
177
- return trailingslashit( $this->multisiteHomeUrlWithoutScheme ) . $this->getSubDir();
178
  }
179
  return $this->multisiteHomeUrlWithoutScheme;
180
  }
181
 
182
  /**
183
- * Get destination Hostname depending on wheather WP has been installed in sub dir or not
184
  * Retun host name without scheme
185
- * @return type
186
  */
187
- private function getDestinationHostname() {
 
 
 
 
 
 
 
 
 
 
 
188
 
189
- if( !empty( $this->options->cloneHostname ) ) {
190
- return $this->strings->getUrlWithoutScheme( $this->options->cloneHostname );
 
191
  }
192
 
193
- if( $this->isSubDir() ) {
194
- return trailingslashit( $this->strings->getUrlWithoutScheme( $this->multisiteDomainWithoutScheme ) ) . $this->getSubDir() . '/' . $this->options->cloneDirectoryName;
 
195
  }
196
 
197
- // Get the path to the main multisite without appending and trailingslash e.g. wordpress
198
- $multisitePath = defined( 'PATH_CURRENT_SITE' ) ? PATH_CURRENT_SITE : '/';
199
- $url = rtrim( $this->strings->getUrlWithoutScheme( $this->multisiteDomainWithoutScheme ), '/\\' ) . $multisitePath . $this->options->cloneDirectoryName;
200
- return $url;
201
  }
202
 
 
203
  /**
204
  * Get the install sub directory if WP is installed in sub directory
205
  * @return string
206
  */
207
- private function getSubDir() {
208
- $home = get_option( 'home' );
209
- $siteurl = get_option( 'siteurl' );
 
210
 
211
- if( empty( $home ) || empty( $siteurl ) ) {
212
  return '';
213
  }
214
 
215
- $dir = str_replace( $home, '', $siteurl );
216
- return str_replace( '/', '', $dir );
217
  }
218
 
219
  /**
@@ -221,14 +243,15 @@ class SearchReplaceExternal extends JobExecutable {
221
  * @param string $new
222
  * @param string $old
223
  */
224
- private function startReplace( $table ) {
 
225
  $rows = $this->options->job->start + $this->settings->querySRLimit;
226
  $this->log(
227
- "DB Search & Replace: Table {$table} {$this->options->job->start} to {$rows} records"
228
  );
229
 
230
  // Search & Replace
231
- $this->searchReplace( $table, $rows, array() );
232
 
233
  // Set new offset
234
  $this->options->job->start += $this->settings->querySRLimit;
@@ -237,17 +260,18 @@ class SearchReplaceExternal extends JobExecutable {
237
  /**
238
  * Gets the columns in a table.
239
  * @access public
240
- * @param string $table The table to check.
241
  * @return array
242
  */
243
- private function get_columns( $table ) {
 
244
  $primary_key = null;
245
- $columns = array();
246
- $fields = $this->stagingDb->get_results( 'DESCRIBE ' . $table );
247
- if( is_array( $fields ) ) {
248
- foreach ( $fields as $column ) {
249
  $columns[] = $column->Field;
250
- if( $column->Key == 'PRI' ) {
251
  $primary_key = $column->Field;
252
  }
253
  }
@@ -264,190 +288,191 @@ class SearchReplaceExternal extends JobExecutable {
264
  * @link https://interconnectit.com/products/search-and-replace-for-wordpress-databases/
265
  *
266
  * @access public
267
- * @param string $table The table to run the replacement on.
268
- * @param int $page The page/block to begin the query on.
269
- * @param array $args An associative array containing arguments for this run.
270
  * @return array
271
  */
272
  private function searchReplace($table, $page, $args)
273
  {
274
 
275
- if ($this->thirdParty->isSearchReplaceExcluded($table)) {
276
  $this->log("DB Search & Replace: Skip {$table}", \WPStaging\Utils\Logger::TYPE_INFO);
277
- return true;
278
- }
279
 
280
  $table = esc_sql($table);
281
 
282
  $args['search_for'] = array(
283
- '%2F%2F'.str_replace('/', '%2F', $this->sourceHostname), // HTML entitity for WP Backery Page Builder Plugin
284
- '\/\/'.str_replace('/', '\/', $this->sourceHostname), // Escaped \/ used by revslider and several visual editors
285
- '//'.$this->sourceHostname, // //example.com
286
  ABSPATH
287
- );
288
 
289
- $args['replace_with'] = array(
290
- '%2F%2F'.str_replace('/', '%2F', $this->destinationHostname),
291
- '\/\/'.str_replace('/', '\/', $this->destinationHostname),
292
- '//'.$this->destinationHostname,
293
  $this->options->destinationDir
294
- );
295
 
296
- $this->debugLog("DB Search & Replace: Search: {$args['search_for'][0]}", \WPStaging\Utils\Logger::TYPE_INFO);
297
- $this->debugLog("DB Search & Replace: Replace: {$args['replace_with'][0]}", \WPStaging\Utils\Logger::TYPE_INFO);
298
 
299
 
300
- $args['replace_guids'] = 'off';
301
- $args['dry_run'] = 'off';
302
- $args['case_insensitive'] = false;
303
- $args['skip_transients'] = 'on';
304
 
305
 
306
- // Allow filtering of search & replace parameters
307
- $args = apply_filters('wpstg_clone_searchreplace_params', $args);
308
 
309
  // Get columns and primary keys
310
- list( $primary_key, $columns ) = $this->get_columns($table);
311
 
312
- // Bail out early if there isn't a primary key.
313
- // We commented this to search & replace through tables which have no primary keys like wp_revslider_slides
314
- // @todo test this carefully. If it causes (performance) issues we need to activate it again!
315
- // @since 2.4.4
316
- // if( null === $primary_key ) {
317
- // return false;
318
- // }
319
 
320
  $current_row = 0;
321
- $start = $this->options->job->start;
322
- $end = $this->settings->querySRLimit;
323
-
324
- $data = $this->stagingDb->get_results( "SELECT * FROM $table LIMIT $start, $end", ARRAY_A );
325
-
326
-
327
- // Filter certain rows (of other plugins)
328
- $filter = array(
329
- 'Admin_custome_login_Slidshow',
330
- 'Admin_custome_login_Social',
331
- 'Admin_custome_login_logo',
332
- 'Admin_custome_login_text',
333
- 'Admin_custome_login_login',
334
- 'Admin_custome_login_top',
335
- 'Admin_custome_login_dashboard',
336
- 'Admin_custome_login_Version',
337
  'upload_path',
338
- 'wpstg_existing_clones_beta',
339
- 'wpstg_existing_clones',
340
- 'wpstg_settings',
341
- 'wpstg_license_status',
342
- 'siteurl',
343
- 'home'
344
- );
345
 
346
- $filter = apply_filters('wpstg_clone_searchreplace_excl_rows', $filter);
347
 
348
  // Go through the table rows
349
- foreach ($data as $row) {
350
- $current_row++;
351
- $update_sql = array();
352
- $where_sql = array();
353
- $upd = false;
354
 
355
  // Skip rows
356
- if (isset($row['option_name']) && in_array($row['option_name'], $filter)) {
357
- continue;
358
- }
359
 
360
  // Skip transients (There can be thousands of them. Save memory and increase performance)
361
- if (isset($row['option_name']) && 'on' === $args['skip_transients'] && false
362
- !== strpos($row['option_name'], '_transient')) {
363
- continue;
364
- }
365
  // Skip rows with more than 5MB to save memory. These rows contain log data or something similiar but never site relevant data
366
- if (isset($row['option_value']) && strlen($row['option_value']) >= 5000000) {
367
- continue;
368
- }
369
 
370
- // Go through the columns
371
- foreach ($columns as $column) {
372
 
373
- $dataRow = $row[$column];
374
 
375
  // Skip column larger than 5MB
376
- $size = strlen($dataRow);
377
- if ($size >= 5000000) {
378
- continue;
379
- }
380
 
381
  // Skip primary key column
382
- if ($column == $primary_key) {
383
- $where_sql[] = $column.' = "'.$this->mysql_escape_mimic($dataRow).'"';
384
- continue;
385
- }
386
 
387
- // Skip GUIDs by default.
388
- if ('on' !== $args['replace_guids'] && 'guid' == $column) {
389
- continue;
390
- }
391
 
392
 
393
- $i = 0;
394
  foreach ($args['search_for'] as $replace) {
395
  $dataRow = $this->recursive_unserialize_replace($args['search_for'][$i], $args['replace_with'][$i], $dataRow, false, $args['case_insensitive']);
396
- $i++;
397
- }
398
  unset($replace, $i);
399
 
400
- // Something was changed
401
- if ($row[$column] != $dataRow) {
402
- $update_sql[] = $column.' = "'.$this->mysql_escape_mimic($dataRow).'"';
403
- $upd = true;
404
- }
405
- }
406
-
407
- // Determine what to do with updates.
408
- if ($args['dry_run'] === 'on') {
409
- // Don't do anything if a dry run
410
- } elseif ($upd && !empty($where_sql)) {
411
- // If there are changes to make, run the query.
412
- $sql = 'UPDATE '.$table.' SET '.implode(', ', $update_sql).' WHERE '.implode(' AND ', array_filter($where_sql));
413
- $result = $this->stagingDb->query($sql);
414
-
415
- if (!$result) {
416
- $this->log("Error updating row {$current_row} SQL: {$sql}", \WPStaging\Utils\Logger::TYPE_ERROR);
417
- }
418
- }
419
- } // end row loop
420
- unset($row);
421
- unset($update_sql);
422
- unset($where_sql);
423
- unset($sql);
424
- unset($current_row);
425
-
426
- // DB Flush
427
- $this->stagingDb->flush();
428
- return true;
429
  }
430
 
431
  /**
432
  * Get path to multisite image folder e.g. wp-content/blogs.dir/ID/files or wp-content/uploads/sites/ID
433
  * @return string
434
  */
435
- private function getImagePathLive() {
 
436
  // Check first which structure is used
437
  $uploads = wp_upload_dir();
438
  $basedir = $uploads['basedir'];
439
- $blogId = get_current_blog_id();
440
 
441
- if( false === strpos( $basedir, 'blogs.dir' ) ) {
442
  // Since WP 3.5
443
  $path = $blogId > 1 ?
444
- 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'sites' . DIRECTORY_SEPARATOR . get_current_blog_id() . DIRECTORY_SEPARATOR :
445
- 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR;
446
  } else {
447
  // old blog structure
448
  $path = $blogId > 1 ?
449
- 'wp-content' . DIRECTORY_SEPARATOR . 'blogs.dir' . DIRECTORY_SEPARATOR . get_current_blog_id() . DIRECTORY_SEPARATOR . 'files' . DIRECTORY_SEPARATOR :
450
- 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR;
451
  }
452
  return $path;
453
  }
@@ -456,7 +481,8 @@ class SearchReplaceExternal extends JobExecutable {
456
  * Get path to staging site image path wp-content/uploads
457
  * @return string
458
  */
459
- private function getImagePathStaging() {
 
460
  return 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR;
461
  }
462
 
@@ -469,68 +495,69 @@ class SearchReplaceExternal extends JobExecutable {
469
  * unserialising any subordinate arrays and performing the replace on those too.
470
  *
471
  * @access private
472
- * @param string $from String we're looking to replace.
473
- * @param string $to What we want it to be replaced with
474
- * @param array $data Used to pass any subordinate arrays back to in.
475
- * @param boolean $serialized Does the array passed via $data need serialising.
476
- * @param sting|boolean $case_insensitive Set to 'on' if we should ignore case, false otherwise.
477
  *
478
- * @return string|array The original array with all elements replaced as needed.
479
  */
480
- private function recursive_unserialize_replace( $from = '', $to = '', $data = '', $serialized = false, $case_insensitive = false ) {
 
481
  try {
482
  // PDO instances can not be serialized or unserialized
483
- if( is_serialized( $data ) && strpos( $data, 'O:3:"PDO":0:' ) !== false ) {
484
  return $data;
485
  }
486
  // DateTime object can not be unserialized.
487
  // Would throw PHP Fatal error: Uncaught Error: Invalid serialization data for DateTime object in
488
  // Bug PHP https://bugs.php.net/bug.php?id=68889&thanks=6 and https://github.com/WP-Staging/wp-staging-pro/issues/74
489
- if( is_serialized( $data ) && strpos( $data, 'O:8:"DateTime":0:' ) !== false ) {
490
  return $data;
491
  }
492
  // Some unserialized data cannot be re-serialized eg. SimpleXMLElements
493
- if( is_serialized( $data ) && ( $unserialized = @unserialize( $data ) ) !== false ) {
494
- $data = $this->recursive_unserialize_replace( $from, $to, $unserialized, true, $case_insensitive );
495
- } elseif( is_array( $data ) ) {
496
  $tmp = array();
497
- foreach ( $data as $key => $value ) {
498
- $tmp[$key] = $this->recursive_unserialize_replace( $from, $to, $value, false, $case_insensitive );
499
  }
500
 
501
  $data = $tmp;
502
- unset( $tmp );
503
- } elseif( is_object( $data ) ) {
504
- $props = get_object_vars( $data );
505
 
506
  // Do a search & replace
507
- if( empty( $props['__PHP_Incomplete_Class_Name'] ) ) {
508
  $tmp = $data;
509
- foreach ( $props as $key => $value ) {
510
- if( $key === '' || ord( $key[0] ) === 0 ) {
511
  continue;
512
  }
513
- $tmp->$key = $this->recursive_unserialize_replace( $from, $to, $value, false, $case_insensitive );
514
  }
515
- $data = $tmp;
516
- $tmp = '';
517
  $props = '';
518
- unset( $tmp );
519
- unset( $props );
520
  }
521
  } else {
522
- if( is_string( $data ) ) {
523
- if( !empty( $from ) && !empty( $to ) ) {
524
- $data = $this->str_replace( $from, $to, $data, $case_insensitive );
525
  }
526
  }
527
  }
528
 
529
- if( $serialized ) {
530
- return serialize( $data );
531
  }
532
- } catch ( Exception $error ) {
533
-
534
  }
535
 
536
  return $data;
@@ -540,15 +567,16 @@ class SearchReplaceExternal extends JobExecutable {
540
  * Mimics the mysql_real_escape_string function. Adapted from a post by 'feedr' on php.net.
541
  * @link http://php.net/manual/en/function.mysql-real-escape-string.php#101248
542
  * @access public
543
- * @param string $input The string to escape.
544
  * @return string
545
  */
546
- private function mysql_escape_mimic( $input ) {
547
- if( is_array( $input ) ) {
548
- return array_map( __METHOD__, $input );
 
549
  }
550
- if( !empty( $input ) && is_string( $input ) ) {
551
- return str_replace( array('\\', "\0", "\n", "\r", "'", '"', "\x1a"), array('\\\\', '\\0', '\\n', '\\r', "\\'", '\\"', '\\Z'), $input );
552
  }
553
 
554
  return $input;
@@ -558,17 +586,18 @@ class SearchReplaceExternal extends JobExecutable {
558
  * Return unserialized object or array
559
  *
560
  * @param string $serialized_string Serialized string.
561
- * @param string $method The name of the caller method.
562
  *
563
  * @return mixed, false on failure
564
  */
565
- private static function unserialize( $serialized_string ) {
566
- if( !is_serialized( $serialized_string ) ) {
 
567
  return false;
568
  }
569
 
570
- $serialized_string = trim( $serialized_string );
571
- $unserialized_string = @unserialize( $serialized_string );
572
 
573
  return $unserialized_string;
574
  }
@@ -583,23 +612,24 @@ class SearchReplaceExternal extends JobExecutable {
583
  *
584
  * @return string
585
  */
586
- private function str_replace( $from, $to, $data, $case_insensitive = false ) {
 
587
 
588
  // Add filter
589
- $excludes = apply_filters( 'wpstg_clone_searchreplace_excl', array() );
590
 
591
  // Build pattern
592
  $regexExclude = '';
593
- foreach ( $excludes as $exclude ) {
594
  $regexExclude .= $exclude . '(*SKIP)(FAIL)|';
595
  }
596
 
597
- if( 'on' === $case_insensitive ) {
598
  //$data = str_ireplace( $from, $to, $data );
599
- $data = preg_replace( '#' . $regexExclude . preg_quote( $from ) . '#i', $to, $data );
600
  } else {
601
  //$data = str_replace( $from, $to, $data );
602
- $data = preg_replace( '#' . $regexExclude . preg_quote( $from ) . '#', $to, $data );
603
  }
604
 
605
  return $data;
@@ -609,13 +639,14 @@ class SearchReplaceExternal extends JobExecutable {
609
  * Set the job
610
  * @param string $table
611
  */
612
- private function setJob( $table ) {
613
- if( !empty( $this->options->job->current ) ) {
 
614
  return;
615
  }
616
 
617
  $this->options->job->current = $table;
618
- $this->options->job->start = 0;
619
  }
620
 
621
  /**
@@ -624,25 +655,26 @@ class SearchReplaceExternal extends JobExecutable {
624
  * @param string $old
625
  * @return bool
626
  */
627
- private function startJob( $new, $old ) {
 
628
 
629
- if( $this->isExcludedTable( $new ) ) {
630
  return false;
631
  }
632
 
633
  // Table does not exist
634
- $result = $this->productionDb->query( "SHOW TABLES LIKE '{$old}'" );
635
- if( !$result || 0 === $result ) {
636
  return false;
637
  }
638
 
639
- if( 0 != $this->options->job->start ) {
640
  return true;
641
  }
642
 
643
- $this->options->job->total = ( int ) $this->productionDb->get_var( "SELECT COUNT(1) FROM {$old}" );
644
 
645
- if( 0 == $this->options->job->total ) {
646
  $this->finishStep();
647
  return false;
648
  }
@@ -655,19 +687,20 @@ class SearchReplaceExternal extends JobExecutable {
655
  * @param string $table
656
  * @return boolean
657
  */
658
- private function isExcludedTable( $table ) {
 
659
 
660
- $customTables = apply_filters( 'wpstg_clone_searchreplace_tables_exclude', array() );
661
  $defaultTables = array('blogs');
662
 
663
- $tables = array_merge( $customTables, $defaultTables );
664
 
665
  $excludedTables = array();
666
- foreach ( $tables as $key => $value ) {
667
  $excludedTables[] = $this->options->prefix . $value;
668
  }
669
 
670
- if( in_array( $table, $excludedTables ) ) {
671
  return true;
672
  }
673
  return false;
@@ -676,9 +709,10 @@ class SearchReplaceExternal extends JobExecutable {
676
  /**
677
  * Finish the step
678
  */
679
- private function finishStep() {
 
680
  // This job is not finished yet
681
- if( $this->options->job->total > $this->options->job->start ) {
682
  return false;
683
  }
684
 
@@ -695,15 +729,16 @@ class SearchReplaceExternal extends JobExecutable {
695
  * Drop table if necessary
696
  * @param string $new
697
  */
698
- private function dropTable( $new ) {
699
- $old = $this->stagingDb->get_var( $this->stagingDb->prepare( "SHOW TABLES LIKE %s", $new ) );
 
700
 
701
- if( !$this->shouldDropTable( $new, $old ) ) {
702
  return;
703
  }
704
 
705
- $this->log( "DB Search & Replace: {$new} already exists, dropping it first" );
706
- $this->stagingDb->query( "DROP TABLE {$new}" );
707
  }
708
 
709
  /**
@@ -712,28 +747,30 @@ class SearchReplaceExternal extends JobExecutable {
712
  * @param string $old
713
  * @return bool
714
  */
715
- private function shouldDropTable( $new, $old ) {
 
716
  return (
717
- $old == $new &&
718
- (
719
- !isset( $this->options->job->current ) ||
720
- !isset( $this->options->job->start ) ||
721
  0 == $this->options->job->start
722
- )
723
- );
724
  }
725
 
726
  /**
727
  * Check if WP is installed in subdir
728
  * @return boolean
729
  */
730
- private function isSubDir() {
 
731
  // Compare names without scheme to bypass cases where siteurl and home have different schemes http / https
732
  // This is happening much more often than you would expect
733
- $siteurl = preg_replace( '#^https?://#', '', rtrim( get_option( 'siteurl' ), '/' ) );
734
- $home = preg_replace( '#^https?://#', '', rtrim( get_option( 'home' ), '/' ) );
735
 
736
- if( $home !== $siteurl ) {
737
  return true;
738
  }
739
  return false;
3
  namespace WPStaging\Backend\Modules\Jobs\Multisite;
4
 
5
  // No Direct Access
6
+ if (!defined("WPINC")) {
7
  die;
8
  }
9
 
15
  * Class Database
16
  * @package WPStaging\Backend\Modules\Jobs
17
  */
18
+ class SearchReplaceExternal extends JobExecutable
19
+ {
20
 
21
  /**
22
  * @var int
55
 
56
  /**
57
  * The prefix of the new database tables which are used for the live site after updating tables
58
+ * @var string
59
  */
60
  public $tmpPrefix;
61
 
62
  /**
63
  * Initialize
64
  */
65
+ public function initialize()
66
+ {
67
+ $this->total = count($this->options->tables);
68
+ $this->stagingDb = $this->getStagingDB();
69
+ $this->productionDb = WPStaging::getInstance()->get("wpdb");
70
+ $this->tmpPrefix = $this->options->prefix;
71
+ $this->strings = new Strings();
72
+ $this->sourceHostname = $this->getSourceHostname();
73
  $this->destinationHostname = $this->getDestinationHostname();
74
  }
75
 
76
  /**
77
  * Get database object to interact with
78
  */
79
+ private function getStagingDB()
80
+ {
81
+ return new \wpdb($this->options->databaseUser, $this->options->databasePassword, $this->options->databaseDatabase, $this->options->databaseServer);
82
  }
83
 
84
+ public function start()
85
+ {
86
  // Skip job. Nothing to do
87
+ if ($this->options->totalSteps === 0) {
88
+ $this->prepareResponse(true, false);
89
  }
90
 
91
  $this->run();
93
  // Save option, progress
94
  $this->saveOptions();
95
 
96
+ return ( object )$this->response;
97
  }
98
 
99
  /**
100
  * Calculate Total Steps in This Job and Assign It to $this->options->totalSteps
101
  * @return void
102
  */
103
+ protected function calculateTotalSteps()
104
+ {
105
  $this->options->totalSteps = $this->total;
106
  }
107
 
110
  * Returns false when over threshold limits are hit or when the job is done, true otherwise
111
  * @return bool
112
  */
113
+ protected function execute()
114
+ {
115
  // Over limits threshold
116
+ if ($this->isOverThreshold()) {
117
  // Prepare response and save current progress
118
+ $this->prepareResponse(false, false);
119
  $this->saveOptions();
120
  return false;
121
  }
122
 
123
  // No more steps, finished
124
+ if ($this->options->currentStep > $this->total || !isset($this->options->tables[$this->options->currentStep])) {
125
+ $this->prepareResponse(true, false);
126
  return false;
127
  }
128
 
129
  // Table is excluded
130
+ if (in_array($this->options->tables[$this->options->currentStep], $this->options->excludedTables)) {
131
  $this->prepareResponse();
132
  return true;
133
  }
134
 
135
  // Search & Replace
136
  if (!$this->updateTable($this->options->tables[$this->options->currentStep])) {
137
+ // Prepare Response
138
+ $this->prepareResponse(false, false);
139
 
140
+ // Not finished
141
+ return true;
142
+ }
143
 
144
 
145
+ // Prepare Response
146
  $this->prepareResponse();
147
 
148
  // Not finished
154
  * @param string $tableName
155
  * @return bool
156
  */
157
+ private function updateTable($tableName)
158
+ {
159
+ $strings = new Strings();
160
+ $table = $strings->str_replace_first($this->productionDb->prefix, '', $tableName);
161
  $newTableName = $this->tmpPrefix . $table;
162
 
163
  // Save current job
164
+ $this->setJob($newTableName);
165
 
166
  // Beginning of the job
167
+ if (!$this->startJob($newTableName, $tableName)) {
168
  return true;
169
  }
170
  // Copy data
171
+ $this->startReplace($newTableName);
172
 
173
  // Finish the step
174
  return $this->finishStep();
178
  * Get source Hostname depending on wheather WP has been installed in sub dir or not
179
  * @return type
180
  */
181
+ private function getSourceHostname()
182
+ {
183
 
184
+ if ($this->isSubDir()) {
185
+ return trailingslashit($this->multisiteHomeUrlWithoutScheme) . $this->getSubDir();
186
  }
187
  return $this->multisiteHomeUrlWithoutScheme;
188
  }
189
 
190
  /**
191
+ * Get destination Hostname depending on WP installed in sub dir or not
192
  * Retun host name without scheme
193
+ * @return string
194
  */
195
+ private function getDestinationHostname()
196
+ {
197
+
198
+ // Staging site is updated so do not change hostname
199
+ if ($this->options->mainJob === 'updating') {
200
+ // If target hostname is defined in advanced settings prefer its use (pro only)
201
+ if (!empty($this->options->cloneHostname)) {
202
+ return $this->strings->getUrlWithoutScheme($this->options->cloneHostname);
203
+ } else {
204
+ return $this->strings->getUrlWithoutScheme($this->options->destinationHostname);
205
+ }
206
+ }
207
 
208
+ // Target hostname defined in advanced settings (pro only)
209
+ if (!empty($this->options->cloneHostname)) {
210
+ return $this->strings->getUrlWithoutScheme($this->options->cloneHostname);
211
  }
212
 
213
+ // WP installed in sub directory under root
214
+ if ($this->isSubDir()) {
215
+ return trailingslashit($this->strings->getUrlWithoutScheme(get_home_url())) . $this->getSubDir() . '/' . $this->options->cloneDirectoryName;
216
  }
217
 
218
+ // Default: Path to root of main multisite without leading or trailing slash e.g.: wordpress
219
+ $multisitePath = defined('PATH_CURRENT_SITE') ? PATH_CURRENT_SITE : '/';
220
+ return rtrim($this->strings->getUrlWithoutScheme($this->multisiteDomainWithoutScheme), '/\\') . $multisitePath . $this->options->cloneDirectoryName;
 
221
  }
222
 
223
+
224
  /**
225
  * Get the install sub directory if WP is installed in sub directory
226
  * @return string
227
  */
228
+ private function getSubDir()
229
+ {
230
+ $home = get_option('home');
231
+ $siteurl = get_option('siteurl');
232
 
233
+ if (empty($home) || empty($siteurl)) {
234
  return '';
235
  }
236
 
237
+ $dir = str_replace($home, '', $siteurl);
238
+ return str_replace('/', '', $dir);
239
  }
240
 
241
  /**
243
  * @param string $new
244
  * @param string $old
245
  */
246
+ private function startReplace($table)
247
+ {
248
  $rows = $this->options->job->start + $this->settings->querySRLimit;
249
  $this->log(
250
+ "DB Search & Replace: Table {$table} {$this->options->job->start} to {$rows} records"
251
  );
252
 
253
  // Search & Replace
254
+ $this->searchReplace($table, $rows, array());
255
 
256
  // Set new offset
257
  $this->options->job->start += $this->settings->querySRLimit;
260
  /**
261
  * Gets the columns in a table.
262
  * @access public
263
+ * @param string $table The table to check.
264
  * @return array
265
  */
266
+ private function get_columns($table)
267
+ {
268
  $primary_key = null;
269
+ $columns = array();
270
+ $fields = $this->stagingDb->get_results('DESCRIBE ' . $table);
271
+ if (is_array($fields)) {
272
+ foreach ($fields as $column) {
273
  $columns[] = $column->Field;
274
+ if ($column->Key == 'PRI') {
275
  $primary_key = $column->Field;
276
  }
277
  }
288
  * @link https://interconnectit.com/products/search-and-replace-for-wordpress-databases/
289
  *
290
  * @access public
291
+ * @param string $table The table to run the replacement on.
292
+ * @param int $page The page/block to begin the query on.
293
+ * @param array $args An associative array containing arguments for this run.
294
  * @return array
295
  */
296
  private function searchReplace($table, $page, $args)
297
  {
298
 
299
+ if ($this->thirdParty->isSearchReplaceExcluded($table)) {
300
  $this->log("DB Search & Replace: Skip {$table}", \WPStaging\Utils\Logger::TYPE_INFO);
301
+ return true;
302
+ }
303
 
304
  $table = esc_sql($table);
305
 
306
  $args['search_for'] = array(
307
+ '%2F%2F' . str_replace('/', '%2F', $this->sourceHostname), // HTML entitity for WP Backery Page Builder Plugin
308
+ '\/\/' . str_replace('/', '\/', $this->sourceHostname), // Escaped \/ used by revslider and several visual editors
309
+ '//' . $this->sourceHostname, // //example.com
310
  ABSPATH
311
+ );
312
 
313
+ $args['replace_with'] = array(
314
+ '%2F%2F' . str_replace('/', '%2F', $this->destinationHostname),
315
+ '\/\/' . str_replace('/', '\/', $this->destinationHostname),
316
+ '//' . $this->destinationHostname,
317
  $this->options->destinationDir
318
+ );
319
 
320
+ $this->debugLog("DB Search & Replace: Search: {$args['search_for'][0]}", \WPStaging\Utils\Logger::TYPE_INFO);
321
+ $this->debugLog("DB Search & Replace: Replace: {$args['replace_with'][0]}", \WPStaging\Utils\Logger::TYPE_INFO);
322
 
323
 
324
+ $args['replace_guids'] = 'off';
325
+ $args['dry_run'] = 'off';
326
+ $args['case_insensitive'] = false;
327
+ $args['skip_transients'] = 'on';
328
 
329
 
330
+ // Allow filtering of search & replace parameters
331
+ $args = apply_filters('wpstg_clone_searchreplace_params', $args);
332
 
333
  // Get columns and primary keys
334
+ list($primary_key, $columns) = $this->get_columns($table);
335
 
336
+ // Bail out early if there isn't a primary key.
337
+ // We commented this to search & replace through tables which have no primary keys like wp_revslider_slides
338
+ // @todo test this carefully. If it causes (performance) issues we need to activate it again!
339
+ // @since 2.4.4
340
+ // if( null === $primary_key ) {
341
+ // return false;
342
+ // }
343
 
344
  $current_row = 0;
345
+ $start = $this->options->job->start;
346
+ $end = $this->settings->querySRLimit;
347
+
348
+ $data = $this->stagingDb->get_results("SELECT * FROM $table LIMIT $start, $end", ARRAY_A);
349
+
350
+
351
+ // Filter certain rows (of other plugins)
352
+ $filter = array(
353
+ 'Admin_custome_login_Slidshow',
354
+ 'Admin_custome_login_Social',
355
+ 'Admin_custome_login_logo',
356
+ 'Admin_custome_login_text',
357
+ 'Admin_custome_login_login',
358
+ 'Admin_custome_login_top',
359
+ 'Admin_custome_login_dashboard',
360
+ 'Admin_custome_login_Version',
361
  'upload_path',
362
+ 'wpstg_existing_clones_beta',
363
+ 'wpstg_existing_clones',
364
+ 'wpstg_settings',
365
+ 'wpstg_license_status',
366
+ 'siteurl',
367
+ 'home'
368
+ );
369
 
370
+ $filter = apply_filters('wpstg_clone_searchreplace_excl_rows', $filter);
371
 
372
  // Go through the table rows
373
+ foreach ($data as $row) {
374
+ $current_row++;
375
+ $update_sql = array();
376
+ $where_sql = array();
377
+ $upd = false;
378
 
379
  // Skip rows
380
+ if (isset($row['option_name']) && in_array($row['option_name'], $filter)) {
381
+ continue;
382
+ }
383
 
384
  // Skip transients (There can be thousands of them. Save memory and increase performance)
385
+ if (isset($row['option_name']) && 'on' === $args['skip_transients'] && false
386
+ !== strpos($row['option_name'], '_transient')) {
387
+ continue;
388
+ }
389
  // Skip rows with more than 5MB to save memory. These rows contain log data or something similiar but never site relevant data
390
+ if (isset($row['option_value']) && strlen($row['option_value']) >= 5000000) {
391
+ continue;
392
+ }
393
 
394
+ // Go through the columns
395
+ foreach ($columns as $column) {
396
 
397
+ $dataRow = $row[$column];
398
 
399
  // Skip column larger than 5MB
400
+ $size = strlen($dataRow);
401
+ if ($size >= 5000000) {
402
+ continue;
403
+ }
404
 
405
  // Skip primary key column
406
+ if ($column == $primary_key) {
407
+ $where_sql[] = $column . ' = "' . $this->mysql_escape_mimic($dataRow) . '"';
408
+ continue;
409
+ }
410
 
411
+ // Skip GUIDs by default.
412
+ if ('on' !== $args['replace_guids'] && 'guid' == $column) {
413
+ continue;
414
+ }
415
 
416
 
417
+ $i = 0;
418
  foreach ($args['search_for'] as $replace) {
419
  $dataRow = $this->recursive_unserialize_replace($args['search_for'][$i], $args['replace_with'][$i], $dataRow, false, $args['case_insensitive']);
420
+ $i++;
421
+ }
422
  unset($replace, $i);
423
 
424
+ // Something was changed
425
+ if ($row[$column] != $dataRow) {
426
+ $update_sql[] = $column . ' = "' . $this->mysql_escape_mimic($dataRow) . '"';
427
+ $upd = true;
428
+ }
429
+ }
430
+
431
+ // Determine what to do with updates.
432
+ if ($args['dry_run'] === 'on') {
433
+ // Don't do anything if a dry run
434
+ } elseif ($upd && !empty($where_sql)) {
435
+ // If there are changes to make, run the query.
436
+ $sql = 'UPDATE ' . $table . ' SET ' . implode(', ', $update_sql) . ' WHERE ' . implode(' AND ', array_filter($where_sql));
437
+ $result = $this->stagingDb->query($sql);
438
+
439
+ if (!$result) {
440
+ $this->log("Error updating row {$current_row} SQL: {$sql}", \WPStaging\Utils\Logger::TYPE_ERROR);
441
+ }
442
+ }
443
+ } // end row loop
444
+ unset($row);
445
+ unset($update_sql);
446
+ unset($where_sql);
447
+ unset($sql);
448
+ unset($current_row);
449
+
450
+ // DB Flush
451
+ $this->stagingDb->flush();
452
+ return true;
453
  }
454
 
455
  /**
456
  * Get path to multisite image folder e.g. wp-content/blogs.dir/ID/files or wp-content/uploads/sites/ID
457
  * @return string
458
  */
459
+ private function getImagePathLive()
460
+ {
461
  // Check first which structure is used
462
  $uploads = wp_upload_dir();
463
  $basedir = $uploads['basedir'];
464
+ $blogId = get_current_blog_id();
465
 
466
+ if (false === strpos($basedir, 'blogs.dir')) {
467
  // Since WP 3.5
468
  $path = $blogId > 1 ?
469
+ 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'sites' . DIRECTORY_SEPARATOR . get_current_blog_id() . DIRECTORY_SEPARATOR :
470
+ 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR;
471
  } else {
472
  // old blog structure
473
  $path = $blogId > 1 ?
474
+ 'wp-content' . DIRECTORY_SEPARATOR . 'blogs.dir' . DIRECTORY_SEPARATOR . get_current_blog_id() . DIRECTORY_SEPARATOR . 'files' . DIRECTORY_SEPARATOR :
475
+ 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR;
476
  }
477
  return $path;
478
  }
481
  * Get path to staging site image path wp-content/uploads
482
  * @return string
483
  */
484
+ private function getImagePathStaging()
485
+ {
486
  return 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR;
487
  }
488
 
495
  * unserialising any subordinate arrays and performing the replace on those too.
496
  *
497
  * @access private
498
+ * @param string $from String we're looking to replace.
499
+ * @param string $to What we want it to be replaced with
500
+ * @param array $data Used to pass any subordinate arrays back to in.
501
+ * @param boolean $serialized Does the array passed via $data need serialising.
502
+ * @param sting|boolean $case_insensitive Set to 'on' if we should ignore case, false otherwise.
503
  *
504
+ * @return string|array The original array with all elements replaced as needed.
505
  */
506
+ private function recursive_unserialize_replace($from = '', $to = '', $data = '', $serialized = false, $case_insensitive = false)
507
+ {
508
  try {
509
  // PDO instances can not be serialized or unserialized
510
+ if (is_serialized($data) && strpos($data, 'O:3:"PDO":0:') !== false) {
511
  return $data;
512
  }
513
  // DateTime object can not be unserialized.
514
  // Would throw PHP Fatal error: Uncaught Error: Invalid serialization data for DateTime object in
515
  // Bug PHP https://bugs.php.net/bug.php?id=68889&thanks=6 and https://github.com/WP-Staging/wp-staging-pro/issues/74
516
+ if (is_serialized($data) && strpos($data, 'O:8:"DateTime":0:') !== false) {
517
  return $data;
518
  }
519
  // Some unserialized data cannot be re-serialized eg. SimpleXMLElements
520
+ if (is_serialized($data) && ($unserialized = @unserialize($data)) !== false) {
521
+ $data = $this->recursive_unserialize_replace($from, $to, $unserialized, true, $case_insensitive);
522
+ } elseif (is_array($data)) {
523
  $tmp = array();
524
+ foreach ($data as $key => $value) {
525
+ $tmp[$key] = $this->recursive_unserialize_replace($from, $to, $value, false, $case_insensitive);
526
  }
527
 
528
  $data = $tmp;
529
+ unset($tmp);
530
+ } elseif (is_object($data)) {
531
+ $props = get_object_vars($data);
532
 
533
  // Do a search & replace
534
+ if (empty($props['__PHP_Incomplete_Class_Name'])) {
535
  $tmp = $data;
536
+ foreach ($props as $key => $value) {
537
+ if ($key === '' || ord($key[0]) === 0) {
538
  continue;
539
  }
540
+ $tmp->$key = $this->recursive_unserialize_replace($from, $to, $value, false, $case_insensitive);
541
  }
542
+ $data = $tmp;
543
+ $tmp = '';
544
  $props = '';
545
+ unset($tmp);
546
+ unset($props);
547
  }
548
  } else {
549
+ if (is_string($data)) {
550
+ if (!empty($from) && !empty($to)) {
551
+ $data = $this->str_replace($from, $to, $data, $case_insensitive);
552
  }
553
  }
554
  }
555
 
556
+ if ($serialized) {
557
+ return serialize($data);
558
  }
559
+ } catch (Exception $error) {
560
+
561
  }
562
 
563
  return $data;
567
  * Mimics the mysql_real_escape_string function. Adapted from a post by 'feedr' on php.net.
568
  * @link http://php.net/manual/en/function.mysql-real-escape-string.php#101248
569
  * @access public
570
+ * @param string $input The string to escape.
571
  * @return string
572
  */
573
+ private function mysql_escape_mimic($input)
574
+ {
575
+ if (is_array($input)) {
576
+ return array_map(__METHOD__, $input);
577
  }
578
+ if (!empty($input) && is_string($input)) {
579
+ return str_replace(array('\\', "\0", "\n", "\r", "'", '"', "\x1a"), array('\\\\', '\\0', '\\n', '\\r', "\\'", '\\"', '\\Z'), $input);
580
  }
581
 
582
  return $input;
586
  * Return unserialized object or array
587
  *
588
  * @param string $serialized_string Serialized string.
589
+ * @param string $method The name of the caller method.
590
  *
591
  * @return mixed, false on failure
592
  */
593
+ private static function unserialize($serialized_string)
594
+ {
595
+ if (!is_serialized($serialized_string)) {
596
  return false;
597
  }
598
 
599
+ $serialized_string = trim($serialized_string);
600
+ $unserialized_string = @unserialize($serialized_string);
601
 
602
  return $unserialized_string;
603
  }
612
  *
613
  * @return string
614
  */
615
+ private function str_replace($from, $to, $data, $case_insensitive = false)
616
+ {
617
 
618
  // Add filter
619
+ $excludes = apply_filters('wpstg_clone_searchreplace_excl', array());
620
 
621
  // Build pattern
622
  $regexExclude = '';
623
+ foreach ($excludes as $exclude) {
624
  $regexExclude .= $exclude . '(*SKIP)(FAIL)|';
625
  }
626
 
627
+ if ('on' === $case_insensitive) {
628
  //$data = str_ireplace( $from, $to, $data );
629
+ $data = preg_replace('#' . $regexExclude . preg_quote($from) . '#i', $to, $data);
630
  } else {
631
  //$data = str_replace( $from, $to, $data );
632
+ $data = preg_replace('#' . $regexExclude . preg_quote($from) . '#', $to, $data);
633
  }
634
 
635
  return $data;
639
  * Set the job
640
  * @param string $table
641
  */
642
+ private function setJob($table)
643
+ {
644
+ if (!empty($this->options->job->current)) {
645
  return;
646
  }
647
 
648
  $this->options->job->current = $table;
649
+ $this->options->job->start = 0;
650
  }
651
 
652
  /**
655
  * @param string $old
656
  * @return bool
657
  */
658
+ private function startJob($new, $old)
659
+ {
660
 
661
+ if ($this->isExcludedTable($new)) {
662
  return false;
663
  }
664
 
665
  // Table does not exist
666
+ $result = $this->productionDb->query("SHOW TABLES LIKE '{$old}'");
667
+ if (!$result || 0 === $result) {
668
  return false;
669
  }
670
 
671
+ if (0 != $this->options->job->start) {
672
  return true;
673
  }
674
 
675
+ $this->options->job->total = ( int )$this->productionDb->get_var("SELECT COUNT(1) FROM {$old}");
676
 
677
+ if (0 == $this->options->job->total) {
678
  $this->finishStep();
679
  return false;
680
  }
687
  * @param string $table
688
  * @return boolean
689
  */
690
+ private function isExcludedTable($table)
691
+ {
692
 
693
+ $customTables = apply_filters('wpstg_clone_searchreplace_tables_exclude', array());
694
  $defaultTables = array('blogs');
695
 
696
+ $tables = array_merge($customTables, $defaultTables);
697
 
698
  $excludedTables = array();
699
+ foreach ($tables as $key => $value) {
700
  $excludedTables[] = $this->options->prefix . $value;
701
  }
702
 
703
+ if (in_array($table, $excludedTables)) {
704
  return true;
705
  }
706
  return false;
709
  /**
710
  * Finish the step
711
  */
712
+ private function finishStep()
713
+ {
714
  // This job is not finished yet
715
+ if ($this->options->job->total > $this->options->job->start) {
716
  return false;
717
  }
718
 
729
  * Drop table if necessary
730
  * @param string $new
731
  */
732
+ private function dropTable($new)
733
+ {
734
+ $old = $this->stagingDb->get_var($this->stagingDb->prepare("SHOW TABLES LIKE %s", $new));
735
 
736
+ if (!$this->shouldDropTable($new, $old)) {
737
  return;
738
  }
739
 
740
+ $this->log("DB Search & Replace: {$new} already exists, dropping it first");
741
+ $this->stagingDb->query("DROP TABLE {$new}");
742
  }
743
 
744
  /**
747
  * @param string $old
748
  * @return bool
749
  */
750
+ private function shouldDropTable($new, $old)
751
+ {
752
  return (
753
+ $old == $new &&
754
+ (
755
+ !isset($this->options->job->current) ||
756
+ !isset($this->options->job->start) ||
757
  0 == $this->options->job->start
758
+ )
759
+ );
760
  }
761
 
762
  /**
763
  * Check if WP is installed in subdir
764
  * @return boolean
765
  */
766
+ private function isSubDir()
767
+ {
768
  // Compare names without scheme to bypass cases where siteurl and home have different schemes http / https
769
  // This is happening much more often than you would expect
770
+ $siteurl = preg_replace('#^https?://#', '', rtrim(get_option('siteurl'), '/'));
771
+ $home = preg_replace('#^https?://#', '', rtrim(get_option('home'), '/'));
772
 
773
+ if ($home !== $siteurl) {
774
  return true;
775
  }
776
  return false;
Backend/Modules/Jobs/SearchReplace.php CHANGED
@@ -80,7 +80,7 @@ class SearchReplace extends JobExecutable
80
  // Save option, progress
81
  $this->saveOptions();
82
 
83
- return (object) $this->response;
84
  }
85
 
86
  /**
@@ -189,22 +189,34 @@ class SearchReplace extends JobExecutable
189
  }
190
 
191
  /**
192
- * Get destination Hostname depending on wheather WP has been installed in sub dir or not
193
- * @return type
194
  */
195
  private function getDestinationHostname()
196
  {
197
- // default
198
- $host = trailingslashit($this->options->destinationHostname) . $this->options->cloneDirectoryName;
199
 
 
 
 
 
 
 
 
 
 
 
 
200
  if (!empty($this->options->cloneHostname)) {
201
- $host = $this->options->cloneHostname;
202
  }
203
 
 
204
  if ($this->isSubDir()) {
205
- $host = trailingslashit($this->options->destinationHostname) . $this->getSubDir() . '/' . $this->options->cloneDirectoryName;
206
  }
207
- return $this->strings->getUrlWithoutScheme($host);
 
 
208
  }
209
 
210
  /**
80
  // Save option, progress
81
  $this->saveOptions();
82
 
83
+ return (object)$this->response;
84
  }
85
 
86
  /**
189
  }
190
 
191
  /**
192
+ * Get destination Hostname depending on WP installed in sub dir or not
193
+ * @return string
194
  */
195
  private function getDestinationHostname()
196
  {
 
 
197
 
198
+ // Staging site is updated so do not change hostname
199
+ if ($this->options->mainJob === 'updating') {
200
+ // If target hostname is defined in advanced settings prefer its use (pro only)
201
+ if (!empty($this->options->cloneHostname)) {
202
+ return $this->strings->getUrlWithoutScheme($this->options->cloneHostname);
203
+ } else {
204
+ return $this->strings->getUrlWithoutScheme($this->options->destinationHostname);
205
+ }
206
+ }
207
+
208
+ // Target hostname defined in advanced settings (pro only)
209
  if (!empty($this->options->cloneHostname)) {
210
+ return $this->strings->getUrlWithoutScheme($this->options->cloneHostname);
211
  }
212
 
213
+ // WP installed in sub directory under root
214
  if ($this->isSubDir()) {
215
+ return $this->strings->getUrlWithoutScheme(trailingslashit($this->options->destinationHostname) . $this->getSubDir() . '/' . $this->options->cloneDirectoryName);
216
  }
217
+
218
+ // Default destination hostname
219
+ return $this->strings->getUrlWithoutScheme(trailingslashit($this->options->destinationHostname) . $this->options->cloneDirectoryName);
220
  }
221
 
222
  /**
Backend/Modules/Jobs/SearchReplaceExternal.php CHANGED
@@ -1,714 +1,750 @@
1
- <?php
2
-
3
- namespace WPStaging\Backend\Modules\Jobs;
4
-
5
- // No Direct Access
6
- if( !defined( "WPINC" ) ) {
7
- die;
8
- }
9
-
10
- use WPStaging\WPStaging;
11
- use WPStaging\Utils\Strings;
12
- use WPStaging\Utils\Helper;
13
-
14
- /**
15
- * Class Database
16
- * @package WPStaging\Backend\Modules\Jobs
17
- */
18
- class SearchReplaceExternal extends JobExecutable {
19
-
20
- /**
21
- * @var int
22
- */
23
- private $total = 0;
24
-
25
-
26
- /**
27
- * @var \WPDB
28
- */
29
- private $productionDb;
30
-
31
- /**
32
- * @var \WPDB
33
- */
34
- private $stagingDb;
35
-
36
- /**
37
- *
38
- * @var string
39
- */
40
- private $sourceHostname;
41
-
42
- /**
43
- *
44
- * @var string
45
- */
46
- private $destinationHostname;
47
-
48
- /**
49
- *
50
- * @var Obj
51
- */
52
- private $strings;
53
-
54
- /**
55
- * The prefix of the new database tables which are used for the live site after updating tables
56
- * @var string
57
- */
58
- public $tmpPrefix;
59
-
60
- /**
61
- * Initialize
62
- */
63
- public function initialize() {
64
- $this->total = count( $this->options->tables );
65
- $this->stagingDb = $this->getStagingDB();
66
- $this->productionDb = WPStaging::getInstance()->get( "wpdb" );
67
- $this->tmpPrefix = $this->options->prefix;
68
- $this->strings = new Strings();
69
- $this->sourceHostname = $this->getSourceHostname();
70
- $this->destinationHostname = $this->getDestinationHostname();
71
- }
72
-
73
- /**
74
- * Get database object to interact with
75
- */
76
- private function getStagingDB() {
77
- if(empty($this->options->databaseUser) || empty($this->options->databasePassword) || empty($this->options->databaseDatabase) || empty($this->options->databaseServer) ){
78
- return null;
79
- }
80
- return new \wpdb( $this->options->databaseUser, $this->options->databasePassword, $this->options->databaseDatabase, $this->options->databaseServer );
81
- }
82
-
83
- public function start() {
84
- // Skip job. Nothing to do
85
- if( $this->options->totalSteps === 0 ) {
86
- $this->prepareResponse( true, false );
87
- }
88
-
89
- $this->run();
90
-
91
- // Save option, progress
92
- $this->saveOptions();
93
-
94
- return ( object ) $this->response;
95
- }
96
-
97
- /**
98
- * Calculate Total Steps in This Job and Assign It to $this->options->totalSteps
99
- * @return void
100
- */
101
- protected function calculateTotalSteps() {
102
- $this->options->totalSteps = $this->total;
103
- }
104
-
105
- /**
106
- * Execute the Current Step
107
- * Returns false when over threshold limits are hit or when the job is done, true otherwise
108
- * @return bool
109
- */
110
- protected function execute() {
111
- // Over limits threshold
112
- if( $this->isOverThreshold() ) {
113
- // Prepare response and save current progress
114
- $this->prepareResponse( false, false );
115
- $this->saveOptions();
116
- return false;
117
- }
118
-
119
- // No more steps, finished
120
- if( $this->options->currentStep > $this->total || !isset( $this->options->tables[$this->options->currentStep] ) ) {
121
- $this->prepareResponse( true, false );
122
- return false;
123
- }
124
-
125
- // Table is excluded
126
- if( in_array( $this->options->tables[$this->options->currentStep], $this->options->excludedTables ) ) {
127
- $this->prepareResponse();
128
- return true;
129
- }
130
-
131
- // Search & Replace
132
- if (!$this->updateTable($this->options->tables[$this->options->currentStep])) {
133
- // Prepare Response
134
- $this->prepareResponse(false, false);
135
-
136
- // Not finished
137
- return true;
138
- }
139
-
140
-
141
- // Prepare Response
142
- $this->prepareResponse();
143
-
144
- // Not finished
145
- return true;
146
- }
147
-
148
- /**
149
- * Copy Tables
150
- * @param string $tableName
151
- * @return bool
152
- */
153
- private function updateTable( $tableName ) {
154
- $strings = new Strings();
155
- $table = $strings->str_replace_first( $this->productionDb->prefix, '', $tableName );
156
- $newTableName = $this->tmpPrefix . $table;
157
-
158
- // Save current job
159
- $this->setJob( $newTableName );
160
-
161
- // Beginning of the job
162
- if( !$this->startJob( $newTableName, $tableName ) ) {
163
- return true;
164
- }
165
- // Copy data
166
- $this->startReplace( $newTableName );
167
-
168
- // Finish the step
169
- return $this->finishStep();
170
- }
171
-
172
- /**
173
- * Get source Hostname depending on wheather WP has been installed in sub dir or not
174
- * @return type
175
- */
176
- private function getSourceHostname() {
177
- // default
178
- $host = $this->options->homeHostname;
179
-
180
- if( $this->isSubDir() ) {
181
- $host = trailingslashit($this->options->homeHostname) . $this->getSubDir();
182
- }
183
- return $this->strings->getUrlWithoutScheme( $host );
184
- }
185
-
186
- /**
187
- * Get destination Hostname depending on wheather WP has been installed in sub dir or not
188
- * Retun host name without scheme
189
- * @return type
190
- */
191
- private function getDestinationHostname() {
192
- // default
193
- $host = trailingslashit( $this->options->destinationHostname ) . $this->options->cloneDirectoryName;
194
-
195
- if( !empty( $this->options->cloneHostname ) ) {
196
- $host = $this->options->cloneHostname;
197
- }
198
-
199
- if( $this->isSubDir() ) {
200
- $host = trailingslashit( $this->options->destinationHostname ) . $this->getSubDir() . '/' . $this->options->cloneDirectoryName;
201
- }
202
- return $this->strings->getUrlWithoutScheme( $host );
203
- }
204
-
205
- /**
206
- * Get the install sub directory if WP is installed in sub directory
207
- * @return string
208
- */
209
- private function getSubDir() {
210
- $home = get_option( 'home' );
211
- $siteurl = get_option( 'siteurl' );
212
-
213
- if( empty( $home ) || empty( $siteurl ) ) {
214
- return '';
215
- }
216
-
217
- $dir = str_replace( $home, '', $siteurl );
218
- return str_replace( '/', '', $dir );
219
- }
220
-
221
- /**
222
- * Start search replace job
223
- * @param string $new
224
- * @param string $old
225
- */
226
- private function startReplace( $table ) {
227
- $rows = $this->options->job->start + $this->settings->querySRLimit;
228
- $this->log(
229
- "DB Search & Replace: Table {$table} {$this->options->job->start} to {$rows} records"
230
- );
231
-
232
- // Search & Replace
233
- $this->searchReplace( $table, $rows, array() );
234
-
235
- // Set new offset
236
- $this->options->job->start += $this->settings->querySRLimit;
237
- }
238
-
239
- /**
240
- * Gets the columns in a table.
241
- * @access public
242
- * @param string $table The table to check.
243
- * @return array
244
- */
245
- private function get_columns( $table ) {
246
- $primary_key = null;
247
- $columns = array();
248
- $fields = $this->stagingDb->get_results( 'DESCRIBE ' . $table );
249
- if( is_array( $fields ) ) {
250
- foreach ( $fields as $column ) {
251
- $columns[] = $column->Field;
252
- if( $column->Key == 'PRI' ) {
253
- $primary_key = $column->Field;
254
- }
255
- }
256
- }
257
- return array($primary_key, $columns);
258
- }
259
-
260
- /**
261
- * Adapted from interconnect/it's search/replace script, adapted from Better Search Replace
262
- *
263
- * Modified to use WordPress wpdb functions instead of PHP's native mysql/pdo functions,
264
- * and to be compatible with batch processing.
265
- *
266
- * @link https://interconnectit.com/products/search-and-replace-for-wordpress-databases/
267
- *
268
- * @access public
269
- * @param string $table The table to run the replacement on.
270
- * @param int $page The page/block to begin the query on.
271
- * @param array $args An associative array containing arguments for this run.
272
- * @return array
273
- */
274
- private function searchReplace($table, $page, $args)
275
- {
276
-
277
- if ($this->thirdParty->isSearchReplaceExcluded($table)) {
278
- $this->log("DB Search & Replace: Skip {$table}",
279
- \WPStaging\Utils\Logger::TYPE_INFO);
280
- return true;
281
- }
282
-
283
- $table = esc_sql($table);
284
-
285
- $args['search_for'] = array(
286
- '%2F%2F'.str_replace('/', '%2F', $this->sourceHostname), // HTML entitity for WP Backery Page Builder Plugin
287
- '\/\/'.str_replace('/', '\/', $this->sourceHostname), // Escaped \/ used by revslider and several visual editors
288
- '//'.$this->sourceHostname, // //example.com
289
- ABSPATH
290
- );
291
-
292
- $args['replace_with'] = array(
293
- '%2F%2F'.str_replace('/', '%2F', $this->destinationHostname),
294
- '\/\/'.str_replace('/', '\/', $this->destinationHostname),
295
- '//'.$this->destinationHostname,
296
- $this->options->destinationDir
297
- );
298
-
299
- $this->debugLog("DB Search & Replace: Search: {$args['search_for'][0]}", \WPStaging\Utils\Logger::TYPE_INFO);
300
- $this->debugLog("DB Search & Replace: Replace: {$args['replace_with'][0]}", \WPStaging\Utils\Logger::TYPE_INFO);
301
-
302
-
303
- $args['replace_guids'] = 'off';
304
- $args['dry_run'] = 'off';
305
- $args['case_insensitive'] = false;
306
- $args['skip_transients'] = 'on';
307
-
308
-
309
- // Allow filtering of search & replace parameters
310
- $args = apply_filters('wpstg_clone_searchreplace_params', $args);
311
-
312
- // Get columns and primary keys
313
- list( $primary_key, $columns ) = $this->get_columns($table);
314
-
315
- // Bail out early if there isn't a primary key.
316
- // We commented this to search & replace through tables which have no primary keys like wp_revslider_slides (failure in their db design?)
317
- // @todo test this carefully. If it causes (performance) issues we need to activate it again!
318
- // @since 2.4.4
319
- // if( null === $primary_key ) {
320
- // return false;
321
- // }
322
-
323
- $current_row = 0;
324
- $start = $this->options->job->start;
325
- $end = $this->settings->querySRLimit;
326
-
327
- $data = $this->stagingDb->get_results( "SELECT * FROM $table LIMIT $start, $end", ARRAY_A );
328
-
329
-
330
- // Filter certain rows (of other plugins)
331
- $filter = array(
332
- 'Admin_custome_login_Slidshow',
333
- 'Admin_custome_login_Social',
334
- 'Admin_custome_login_logo',
335
- 'Admin_custome_login_text',
336
- 'Admin_custome_login_login',
337
- 'Admin_custome_login_top',
338
- 'Admin_custome_login_dashboard',
339
- 'Admin_custome_login_Version',
340
- 'upload_path',
341
- 'wpstg_existing_clones_beta',
342
- 'wpstg_existing_clones',
343
- 'wpstg_settings',
344
- 'wpstg_license_status',
345
- 'siteurl',
346
- 'home'
347
- );
348
-
349
- $filter = apply_filters('wpstg_clone_searchreplace_excl_rows', $filter);
350
-
351
- // Go through the table rows
352
- foreach ($data as $row) {
353
- $current_row++;
354
- $update_sql = array();
355
- $where_sql = array();
356
- $upd = false;
357
-
358
- // Skip rows
359
- if (isset($row['option_name']) && in_array($row['option_name'], $filter)) {
360
- continue;
361
- }
362
-
363
- // Skip transients (There can be thousands of them. Save memory and increase performance)
364
- if (isset($row['option_name']) && 'on' === $args['skip_transients'] && false
365
- !== strpos($row['option_name'], '_transient')) {
366
- continue;
367
- }
368
- // Skip rows with more than 5MB to save memory. These rows contain log data or something similiar but never site relevant data
369
- if (isset($row['option_value']) && strlen($row['option_value']) >= 5000000) {
370
- continue;
371
- }
372
-
373
- // Go through the columns
374
- foreach ($columns as $column) {
375
-
376
- $dataRow = $row[$column];
377
-
378
- // Skip column larger than 5MB
379
- $size = strlen($dataRow);
380
- if ($size >= 5000000) {
381
- continue;
382
- }
383
-
384
- // Skip primary key column
385
- if ($column == $primary_key) {
386
- $where_sql[] = $column.' = "'.$this->mysql_escape_mimic($dataRow).'"';
387
- continue;
388
- }
389
-
390
- // Skip GUIDs by default.
391
- if ('on' !== $args['replace_guids'] && 'guid' == $column) {
392
- continue;
393
- }
394
-
395
-
396
- $i = 0;
397
- foreach ($args['search_for'] as $replace) {
398
- $dataRow = $this->recursive_unserialize_replace($args['search_for'][$i], $args['replace_with'][$i], $dataRow, false, $args['case_insensitive']);
399
- $i++;
400
- }
401
- unset($replace, $i);
402
-
403
- // Something was changed
404
- if ($row[$column] != $dataRow) {
405
- $update_sql[] = $column.' = "'.$this->mysql_escape_mimic($dataRow).'"';
406
- $upd = true;
407
- }
408
- }
409
-
410
- // Determine what to do with updates.
411
- if ($args['dry_run'] === 'on') {
412
- // Don't do anything if a dry run
413
- } elseif ($upd && !empty($where_sql)) {
414
- // If there are changes to make, run the query.
415
- $sql = 'UPDATE '.$table.' SET '.implode(', ', $update_sql).' WHERE '.implode(' AND ', array_filter($where_sql));
416
- $result = $this->stagingDb->query($sql);
417
-
418
- if (false === $result) {
419
- $this->log("Error updating row {$current_row} SQL: {$sql}",
420
- \WPStaging\Utils\Logger::TYPE_ERROR);
421
- }
422
- }
423
- } // end row loop
424
- unset($row);
425
- unset($update_sql);
426
- unset($where_sql);
427
- unset($sql);
428
- unset($current_row);
429
-
430
- // DB Flush
431
- $this->stagingDb->flush();
432
- return true;
433
- }
434
-
435
- /**
436
- * Adapted from interconnect/it's search/replace script.
437
- *
438
- * @link https://interconnectit.com/products/search-and-replace-for-wordpress-databases/
439
- *
440
- * Take a serialised array and unserialise it replacing elements as needed and
441
- * unserialising any subordinate arrays and performing the replace on those too.
442
- *
443
- * @access private
444
- * @param string $from String we're looking to replace.
445
- * @param string $to What we want it to be replaced with
446
- * @param array $data Used to pass any subordinate arrays back to in.
447
- * @param boolean $serialized Does the array passed via $data need serialising.
448
- * @param sting|boolean $case_insensitive Set to 'on' if we should ignore case, false otherwise.
449
- *
450
- * @return string|array The original array with all elements replaced as needed.
451
- */
452
- private function recursive_unserialize_replace( $from = '', $to = '', $data = '', $serialized = false, $case_insensitive = false ) {
453
- try {
454
- // PDO instances can not be serialized or unserialized
455
- if( is_serialized( $data ) && strpos( $data, 'O:3:"PDO":0:' ) !== false ) {
456
- return $data;
457
- }
458
- // DateTime object can not be unserialized.
459
- // Would throw PHP Fatal error: Uncaught Error: Invalid serialization data for DateTime object in
460
- // Bug PHP https://bugs.php.net/bug.php?id=68889&thanks=6 and https://github.com/WP-Staging/wp-staging-pro/issues/74
461
- if( is_serialized( $data ) && strpos( $data, 'O:8:"DateTime":0:' ) !== false ) {
462
- return $data;
463
- }
464
- // Some unserialized data cannot be re-serialized eg. SimpleXMLElements
465
- if( is_serialized( $data ) && ( $unserialized = @unserialize( $data ) ) !== false ) {
466
- $data = $this->recursive_unserialize_replace( $from, $to, $unserialized, true, $case_insensitive );
467
- } elseif( is_array( $data ) ) {
468
- $tmp = array();
469
- foreach ( $data as $key => $value ) {
470
- $tmp[$key] = $this->recursive_unserialize_replace( $from, $to, $value, false, $case_insensitive );
471
- }
472
-
473
- $data = $tmp;
474
- unset( $tmp );
475
- } elseif( is_object( $data ) ) {
476
- $props = get_object_vars( $data );
477
-
478
- // Do a search & replace
479
- if( empty( $props['__PHP_Incomplete_Class_Name'] ) ) {
480
- $tmp = $data;
481
- foreach ( $props as $key => $value ) {
482
- if( $key === '' || ord( $key[0] ) === 0 ) {
483
- continue;
484
- }
485
- $tmp->$key = $this->recursive_unserialize_replace( $from, $to, $value, false, $case_insensitive );
486
- }
487
- $data = $tmp;
488
- $tmp = '';
489
- $props = '';
490
- unset( $tmp );
491
- unset( $props );
492
- }
493
- } else {
494
- if( is_string( $data ) ) {
495
- if( !empty( $from ) && !empty( $to ) ) {
496
- $data = $this->str_replace( $from, $to, $data, $case_insensitive );
497
- }
498
- }
499
- }
500
-
501
- if( $serialized ) {
502
- return serialize( $data );
503
- }
504
- } catch ( Exception $error ) {
505
-
506
- }
507
-
508
- return $data;
509
- }
510
-
511
- /**
512
- * Mimics the mysql_real_escape_string function. Adapted from a post by 'feedr' on php.net.
513
- * @link http://php.net/manual/en/function.mysql-real-escape-string.php#101248
514
- * @access public
515
- * @param string $input The string to escape.
516
- * @return string
517
- */
518
- private function mysql_escape_mimic( $input ) {
519
- if( is_array( $input ) ) {
520
- return array_map( __METHOD__, $input );
521
- }
522
- if( !empty( $input ) && is_string( $input ) ) {
523
- return str_replace( array('\\', "\0", "\n", "\r", "'", '"', "\x1a"), array('\\\\', '\\0', '\\n', '\\r', "\\'", '\\"', '\\Z'), $input );
524
- }
525
-
526
- return $input;
527
- }
528
-
529
- /**
530
- * Return unserialized object or array
531
- *
532
- * @param string $serialized_string Serialized string.
533
- * @param string $method The name of the caller method.
534
- *
535
- * @return mixed, false on failure
536
- */
537
- private static function unserialize( $serialized_string ) {
538
- if( !is_serialized( $serialized_string ) ) {
539
- return false;
540
- }
541
-
542
- $serialized_string = trim( $serialized_string );
543
- $unserialized_string = @unserialize( $serialized_string );
544
-
545
- return $unserialized_string;
546
- }
547
-
548
- /**
549
- * Wrapper for str_replace
550
- *
551
- * @param string $from
552
- * @param string $to
553
- * @param string $data
554
- * @param string|bool $case_insensitive
555
- *
556
- * @return string
557
- */
558
- private function str_replace( $from, $to, $data, $case_insensitive = false ) {
559
-
560
- // Add filter
561
- $excludes = apply_filters( 'wpstg_clone_searchreplace_excl', array() );
562
-
563
- // Build pattern
564
- $regexExclude = '';
565
- foreach ( $excludes as $exclude ) {
566
- $regexExclude .= $exclude . '(*SKIP)(FAIL)|';
567
- }
568
-
569
- if( 'on' === $case_insensitive ) {
570
- //$data = str_ireplace( $from, $to, $data );
571
- $data = preg_replace( '#' . $regexExclude . preg_quote( $from ) . '#i', $to, $data );
572
- } else {
573
- //$data = str_replace( $from, $to, $data );
574
- $data = preg_replace( '#' . $regexExclude . preg_quote( $from ) . '#', $to, $data );
575
- }
576
-
577
- return $data;
578
- }
579
-
580
- /**
581
- * Set the job
582
- * @param string $table
583
- */
584
- private function setJob( $table ) {
585
- if( !empty( $this->options->job->current ) ) {
586
- return;
587
- }
588
-
589
- $this->options->job->current = $table;
590
- $this->options->job->start = 0;
591
- }
592
-
593
- /**
594
- * Start Job
595
- * @param string $new
596
- * @param string $old
597
- * @return bool
598
- */
599
- private function startJob( $new, $old ) {
600
-
601
- if( $this->isExcludedTable( $new ) ) {
602
- return false;
603
- }
604
-
605
- // Table does not exist
606
- $result = $this->productionDb->query( "SHOW TABLES LIKE '{$old}'" );
607
- if( !$result || 0 === $result ) {
608
- return false;
609
- }
610
-
611
- if( 0 != $this->options->job->start ) {
612
- return true;
613
- }
614
-
615
- $this->options->job->total = ( int ) $this->productionDb->get_var( "SELECT COUNT(1) FROM {$old}" );
616
-
617
- if( 0 == $this->options->job->total ) {
618
- $this->finishStep();
619
- return false;
620
- }
621
-
622
- return true;
623
- }
624
-
625
- /**
626
- * Is table excluded from search replace processing?
627
- * @param string $table
628
- * @return boolean
629
- */
630
- private function isExcludedTable( $table ) {
631
-
632
- $customTables = apply_filters( 'wpstg_clone_searchreplace_tables_exclude', array() );
633
- $defaultTables = array('blogs');
634
-
635
- $tables = array_merge( $customTables, $defaultTables );
636
-
637
- $excludedTables = array();
638
- foreach ( $tables as $key => $value ) {
639
- $excludedTables[] = $this->options->prefix . $value;
640
- }
641
-
642
- if( in_array( $table, $excludedTables ) ) {
643
- return true;
644
- }
645
- return false;
646
- }
647
-
648
- /**
649
- * Finish the step
650
- */
651
- private function finishStep() {
652
- // This job is not finished yet
653
- if( $this->options->job->total > $this->options->job->start ) {
654
- return false;
655
- }
656
-
657
- // Add it to cloned tables listing
658
- $this->options->clonedTables[] = $this->options->tables[$this->options->currentStep];
659
-
660
- // Reset job
661
- $this->options->job = new \stdClass();
662
-
663
- return true;
664
- }
665
-
666
- /**
667
- * Drop table if necessary
668
- * @param string $new
669
- */
670
- private function dropTable( $new ) {
671
- $old = $this->stagingDb->get_var( $this->stagingDb->prepare( "SHOW TABLES LIKE %s", $new ) );
672
-
673
- if( !$this->shouldDropTable( $new, $old ) ) {
674
- return;
675
- }
676
-
677
- $this->log( "DB Search & Replace: {$new} already exists, dropping it first" );
678
- $this->stagingDb->query( "DROP TABLE {$new}" );
679
- }
680
-
681
- /**
682
- * Check if table needs to be dropped
683
- * @param string $new
684
- * @param string $old
685
- * @return bool
686
- */
687
- private function shouldDropTable( $new, $old ) {
688
- return (
689
- $old == $new &&
690
- (
691
- !isset( $this->options->job->current ) ||
692
- !isset( $this->options->job->start ) ||
693
- 0 == $this->options->job->start
694
- )
695
- );
696
- }
697
-
698
- /**
699
- * Check if WP is installed in subdir
700
- * @return boolean
701
- */
702
- private function isSubDir() {
703
- // Compare names without scheme to bypass cases where siteurl and home have different schemes http / https
704
- // This is happening much more often than you would expect
705
- $siteurl = preg_replace( '#^https?://#', '', rtrim( get_option( 'siteurl' ), '/' ) );
706
- $home = preg_replace( '#^https?://#', '', rtrim( get_option( 'home' ), '/' ) );
707
-
708
- if( $home !== $siteurl ) {
709
- return true;
710
- }
711
- return false;
712
- }
713
-
714
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace WPStaging\Backend\Modules\Jobs;
4
+
5
+ // No Direct Access
6
+ if (!defined("WPINC")) {
7
+ die;
8
+ }
9
+
10
+ use WPStaging\WPStaging;
11
+ use WPStaging\Utils\Strings;
12
+ use WPStaging\Utils\Helper;
13
+
14
+ /**
15
+ * Class Database
16
+ * @package WPStaging\Backend\Modules\Jobs
17
+ */
18
+ class SearchReplaceExternal extends JobExecutable
19
+ {
20
+
21
+ /**
22
+ * @var int
23
+ */
24
+ private $total = 0;
25
+
26
+
27
+ /**
28
+ * @var \WPDB
29
+ */
30
+ private $productionDb;
31
+
32
+ /**
33
+ * @var \WPDB
34
+ */
35
+ private $stagingDb;
36
+
37
+ /**
38
+ *
39
+ * @var string
40
+ */
41
+ private $sourceHostname;
42
+
43
+ /**
44
+ *
45
+ * @var string
46
+ */
47
+ private $destinationHostname;
48
+
49
+ /**
50
+ *
51
+ * @var Obj
52
+ */
53
+ private $strings;
54
+
55
+ /**
56
+ * The prefix of the new database tables which are used for the live site after updating tables
57
+ * @var string
58
+ */
59
+ public $tmpPrefix;
60
+
61
+ /**
62
+ * Initialize
63
+ */
64
+ public function initialize()
65
+ {
66
+ $this->total = count($this->options->tables);
67
+ $this->stagingDb = $this->getStagingDB();
68
+ $this->productionDb = WPStaging::getInstance()->get("wpdb");
69
+ $this->tmpPrefix = $this->options->prefix;
70
+ $this->strings = new Strings();
71
+ $this->sourceHostname = $this->getSourceHostname();
72
+ $this->destinationHostname = $this->getDestinationHostname();
73
+ }
74
+
75
+ /**
76
+ * Get database object to interact with
77
+ */
78
+ private function getStagingDB()
79
+ {
80
+ if (empty($this->options->databaseUser) || empty($this->options->databasePassword) || empty($this->options->databaseDatabase) || empty($this->options->databaseServer)) {
81
+ return null;
82
+ }
83
+ return new \wpdb($this->options->databaseUser, $this->options->databasePassword, $this->options->databaseDatabase, $this->options->databaseServer);
84
+ }
85
+
86
+ public function start()
87
+ {
88
+ // Skip job. Nothing to do
89
+ if ($this->options->totalSteps === 0) {
90
+ $this->prepareResponse(true, false);
91
+ }
92
+
93
+ $this->run();
94
+
95
+ // Save option, progress
96
+ $this->saveOptions();
97
+
98
+ return ( object )$this->response;
99
+ }
100
+
101
+ /**
102
+ * Calculate Total Steps in This Job and Assign It to $this->options->totalSteps
103
+ * @return void
104
+ */
105
+ protected function calculateTotalSteps()
106
+ {
107
+ $this->options->totalSteps = $this->total;
108
+ }
109
+
110
+ /**
111
+ * Execute the Current Step
112
+ * Returns false when over threshold limits are hit or when the job is done, true otherwise
113
+ * @return bool
114
+ */
115
+ protected function execute()
116
+ {
117
+ // Over limits threshold
118
+ if ($this->isOverThreshold()) {
119
+ // Prepare response and save current progress
120
+ $this->prepareResponse(false, false);
121
+ $this->saveOptions();
122
+ return false;
123
+ }
124
+
125
+ // No more steps, finished
126
+ if ($this->options->currentStep > $this->total || !isset($this->options->tables[$this->options->currentStep])) {
127
+ $this->prepareResponse(true, false);
128
+ return false;
129
+ }
130
+
131
+ // Table is excluded
132
+ if (in_array($this->options->tables[$this->options->currentStep], $this->options->excludedTables)) {
133
+ $this->prepareResponse();
134
+ return true;
135
+ }
136
+
137
+ // Search & Replace
138
+ if (!$this->updateTable($this->options->tables[$this->options->currentStep])) {
139
+ // Prepare Response
140
+ $this->prepareResponse(false, false);
141
+
142
+ // Not finished
143
+ return true;
144
+ }
145
+
146
+
147
+ // Prepare Response
148
+ $this->prepareResponse();
149
+
150
+ // Not finished
151
+ return true;
152
+ }
153
+
154
+ /**
155
+ * Copy Tables
156
+ * @param string $tableName
157
+ * @return bool
158
+ */
159
+ private function updateTable($tableName)
160
+ {
161
+ $strings = new Strings();
162
+ $table = $strings->str_replace_first($this->productionDb->prefix, '', $tableName);
163
+ $newTableName = $this->tmpPrefix . $table;
164
+
165
+ // Save current job
166
+ $this->setJob($newTableName);
167
+
168
+ // Beginning of the job
169
+ if (!$this->startJob($newTableName, $tableName)) {
170
+ return true;
171
+ }
172
+ // Copy data
173
+ $this->startReplace($newTableName);
174
+
175
+ // Finish the step
176
+ return $this->finishStep();
177
+ }
178
+
179
+ /**
180
+ * Get source Hostname depending on wheather WP has been installed in sub dir or not
181
+ * @return string
182
+ */
183
+ private function getSourceHostname()
184
+ {
185
+ // default
186
+ $host = $this->options->homeHostname;
187
+
188
+ if ($this->isSubDir()) {
189
+ $host = trailingslashit($this->options->homeHostname) . $this->getSubDir();
190
+ }
191
+ return $this->strings->getUrlWithoutScheme($host);
192
+ }
193
+
194
+ /**
195
+ * Get destination Hostname depending on WP installed in sub dir or not
196
+ * Retun host name without scheme
197
+ * @return string
198
+ */
199
+ private function getDestinationHostname()
200
+ {
201
+
202
+ // Staging site is updated so do not change hostname
203
+ if ($this->options->mainJob === 'updating') {
204
+ // If target hostname is defined in advanced settings prefer its use (pro only)
205
+ if (!empty($this->options->cloneHostname)) {
206
+ return $this->strings->getUrlWithoutScheme($this->options->cloneHostname);
207
+ } else {
208
+ return $this->strings->getUrlWithoutScheme($this->options->destinationHostname);
209
+ }
210
+ }
211
+
212
+ // Target hostname defined in advanced settings (pro only)
213
+ if (!empty($this->options->cloneHostname)) {
214
+ return $this->strings->getUrlWithoutScheme($this->options->cloneHostname);
215
+ }
216
+
217
+ // WP installed in sub directory under root
218
+ if ($this->isSubDir()) {
219
+ return $this->strings->getUrlWithoutScheme(trailingslashit($this->options->destinationHostname) . $this->getSubDir() . '/' . $this->options->cloneDirectoryName);
220
+ }
221
+
222
+ // Default destination hostname
223
+ return $this->strings->getUrlWithoutScheme(trailingslashit($this->options->destinationHostname) . $this->options->cloneDirectoryName);
224
+ }
225
+
226
+
227
+ /**
228
+ * Get the install sub directory if WP is installed in sub directory
229
+ * @return string
230
+ */
231
+ private function getSubDir()
232
+ {
233
+ $home = get_option('home');
234
+ $siteurl = get_option('siteurl');
235
+
236
+ if (empty($home) || empty($siteurl)) {
237
+ return '';
238
+ }
239
+
240
+ $dir = str_replace($home, '', $siteurl);
241
+ return str_replace('/', '', $dir);
242
+ }
243
+
244
+ /**
245
+ * Start search replace job
246
+ * @param string $new
247
+ * @param string $old
248
+ */
249
+ private function startReplace($table)
250
+ {
251
+ $rows = $this->options->job->start + $this->settings->querySRLimit;
252
+ $this->log(
253
+ "DB Search & Replace: Table {$table} {$this->options->job->start} to {$rows} records"
254
+ );
255
+
256
+ // Search & Replace
257
+ $this->searchReplace($table, $rows, array());
258
+
259
+ // Set new offset
260
+ $this->options->job->start += $this->settings->querySRLimit;
261
+ }
262
+
263
+ /**
264
+ * Gets the columns in a table.
265
+ * @access public
266
+ * @param string $table The table to check.
267
+ * @return array
268
+ */
269
+ private function get_columns($table)
270
+ {
271
+ $primary_key = null;
272
+ $columns = array();
273
+ $fields = $this->stagingDb->get_results('DESCRIBE ' . $table);
274
+ if (is_array($fields)) {
275
+ foreach ($fields as $column) {
276
+ $columns[] = $column->Field;
277
+ if ($column->Key == 'PRI') {
278
+ $primary_key = $column->Field;
279
+ }
280
+ }
281
+ }
282
+ return array($primary_key, $columns);
283
+ }
284
+
285
+ /**
286
+ * Adapted from interconnect/it's search/replace script, adapted from Better Search Replace
287
+ *
288
+ * Modified to use WordPress wpdb functions instead of PHP's native mysql/pdo functions,
289
+ * and to be compatible with batch processing.
290
+ *
291
+ * @link https://interconnectit.com/products/search-and-replace-for-wordpress-databases/
292
+ *
293
+ * @access public
294
+ * @param string $table The table to run the replacement on.
295
+ * @param int $page The page/block to begin the query on.
296
+ * @param array $args An associative array containing arguments for this run.
297
+ * @return array
298
+ */
299
+ private function searchReplace($table, $page, $args)
300
+ {
301
+
302
+ if ($this->thirdParty->isSearchReplaceExcluded($table)) {
303
+ $this->log("DB Search & Replace: Skip {$table}",
304
+ \WPStaging\Utils\Logger::TYPE_INFO);
305
+ return true;
306
+ }
307
+
308
+ $table = esc_sql($table);
309
+
310
+ $args['search_for'] = array(
311
+ '%2F%2F' . str_replace('/', '%2F', $this->sourceHostname), // HTML entitity for WP Backery Page Builder Plugin
312
+ '\/\/' . str_replace('/', '\/', $this->sourceHostname), // Escaped \/ used by revslider and several visual editors
313
+ '//' . $this->sourceHostname, // //example.com
314
+ ABSPATH
315
+ );
316
+
317
+ $args['replace_with'] = array(
318
+ '%2F%2F' . str_replace('/', '%2F', $this->destinationHostname),
319
+ '\/\/' . str_replace('/', '\/', $this->destinationHostname),
320
+ '//' . $this->destinationHostname,
321
+ $this->options->destinationDir
322
+ );
323
+
324
+ $this->debugLog("DB Search & Replace: Search: {$args['search_for'][0]}", \WPStaging\Utils\Logger::TYPE_INFO);
325
+ $this->debugLog("DB Search & Replace: Replace: {$args['replace_with'][0]}", \WPStaging\Utils\Logger::TYPE_INFO);
326
+
327
+
328
+ $args['replace_guids'] = 'off';
329
+ $args['dry_run'] = 'off';
330
+ $args['case_insensitive'] = false;
331
+ $args['skip_transients'] = 'on';
332
+
333
+
334
+ // Allow filtering of search & replace parameters
335
+ $args = apply_filters('wpstg_clone_searchreplace_params', $args);
336
+
337
+ // Get columns and primary keys
338
+ list($primary_key, $columns) = $this->get_columns($table);
339
+
340
+ // Bail out early if there isn't a primary key.
341
+ // We commented this to search & replace through tables which have no primary keys like wp_revslider_slides (failure in their db design?)
342
+ // @todo test this carefully. If it causes (performance) issues we need to activate it again!
343
+ // @since 2.4.4
344
+ // if( null === $primary_key ) {
345
+ // return false;
346
+ // }
347
+
348
+ $current_row = 0;
349
+ $start = $this->options->job->start;
350
+ $end = $this->settings->querySRLimit;
351
+
352
+ $data = $this->stagingDb->get_results("SELECT * FROM $table LIMIT $start, $end", ARRAY_A);
353
+
354
+
355
+ // Filter certain rows (of other plugins)
356
+ $filter = array(
357
+ 'Admin_custome_login_Slidshow',
358
+ 'Admin_custome_login_Social',
359
+ 'Admin_custome_login_logo',
360
+ 'Admin_custome_login_text',
361
+ 'Admin_custome_login_login',
362
+ 'Admin_custome_login_top',
363
+ 'Admin_custome_login_dashboard',
364
+ 'Admin_custome_login_Version',
365
+ 'upload_path',
366
+ 'wpstg_existing_clones_beta',
367
+ 'wpstg_existing_clones',
368
+ 'wpstg_settings',
369
+ 'wpstg_license_status',
370
+ 'siteurl',
371
+ 'home'
372
+ );
373
+
374
+ $filter = apply_filters('wpstg_clone_searchreplace_excl_rows', $filter);
375
+
376
+ // Go through the table rows
377
+ foreach ($data as $row) {
378
+ $current_row++;
379
+ $update_sql = array();
380
+ $where_sql = array();
381
+ $upd = false;
382
+
383
+ // Skip rows
384
+ if (isset($row['option_name']) && in_array($row['option_name'], $filter)) {
385
+ continue;
386
+ }
387
+
388
+ // Skip transients (There can be thousands of them. Save memory and increase performance)
389
+ if (isset($row['option_name']) && 'on' === $args['skip_transients'] && false
390
+ !== strpos($row['option_name'], '_transient')) {
391
+ continue;
392
+ }
393
+ // Skip rows with more than 5MB to save memory. These rows contain log data or something similiar but never site relevant data
394
+ if (isset($row['option_value']) && strlen($row['option_value']) >= 5000000) {
395
+ continue;
396
+ }
397
+
398
+ // Go through the columns
399
+ foreach ($columns as $column) {
400
+
401
+ $dataRow = $row[$column];
402
+
403
+ // Skip column larger than 5MB
404
+ $size = strlen($dataRow);
405
+ if ($size >= 5000000) {
406
+ continue;
407
+ }
408
+
409
+ // Skip primary key column
410
+ if ($column == $primary_key) {
411
+ $where_sql[] = $column . ' = "' . $this->mysql_escape_mimic($dataRow) . '"';
412
+ continue;
413
+ }
414
+
415
+ // Skip GUIDs by default.
416
+ if ('on' !== $args['replace_guids'] && 'guid' == $column) {
417
+ continue;
418
+ }
419
+
420
+
421
+ $i = 0;
422
+ foreach ($args['search_for'] as $replace) {
423
+ $dataRow = $this->recursive_unserialize_replace($args['search_for'][$i], $args['replace_with'][$i], $dataRow, false, $args['case_insensitive']);
424
+ $i++;
425
+ }
426
+ unset($replace, $i);
427
+
428
+ // Something was changed
429
+ if ($row[$column] != $dataRow) {
430
+ $update_sql[] = $column . ' = "' . $this->mysql_escape_mimic($dataRow) . '"';
431
+ $upd = true;
432
+ }
433
+ }
434
+
435
+ // Determine what to do with updates.
436
+ if ($args['dry_run'] === 'on') {
437
+ // Don't do anything if a dry run
438
+ } elseif ($upd && !empty($where_sql)) {
439
+ // If there are changes to make, run the query.
440
+ $sql = 'UPDATE ' . $table . ' SET ' . implode(', ', $update_sql) . ' WHERE ' . implode(' AND ', array_filter($where_sql));
441
+ $result = $this->stagingDb->query($sql);
442
+
443
+ if (false === $result) {
444
+ $this->log("Error updating row {$current_row} SQL: {$sql}",
445
+ \WPStaging\Utils\Logger::TYPE_ERROR);
446
+ }
447
+ }
448
+ } // end row loop
449
+ unset($row);
450
+ unset($update_sql);
451
+ unset($where_sql);
452
+ unset($sql);
453
+ unset($current_row);
454
+
455
+ // DB Flush
456
+ $this->stagingDb->flush();
457
+ return true;
458
+ }
459
+
460
+ /**
461
+ * Adapted from interconnect/it's search/replace script.
462
+ *
463
+ * @link https://interconnectit.com/products/search-and-replace-for-wordpress-databases/
464
+ *
465
+ * Take a serialised array and unserialise it replacing elements as needed and
466
+ * unserialising any subordinate arrays and performing the replace on those too.
467
+ *
468
+ * @access private
469
+ * @param string $from String we're looking to replace.
470
+ * @param string $to What we want it to be replaced with
471
+ * @param array $data Used to pass any subordinate arrays back to in.
472
+ * @param boolean $serialized Does the array passed via $data need serialising.
473
+ * @param sting|boolean $case_insensitive Set to 'on' if we should ignore case, false otherwise.
474
+ *
475
+ * @return string|array The original array with all elements replaced as needed.
476
+ */
477
+ private function recursive_unserialize_replace($from = '', $to = '', $data = '', $serialized = false, $case_insensitive = false)
478
+ {
479
+ try {
480
+ // PDO instances can not be serialized or unserialized
481
+ if (is_serialized($data) && strpos($data, 'O:3:"PDO":0:') !== false) {
482
+ return $data;
483
+ }
484
+ // DateTime object can not be unserialized.
485
+ // Would throw PHP Fatal error: Uncaught Error: Invalid serialization data for DateTime object in
486
+ // Bug PHP https://bugs.php.net/bug.php?id=68889&thanks=6 and https://github.com/WP-Staging/wp-staging-pro/issues/74
487
+ if (is_serialized($data) && strpos($data, 'O:8:"DateTime":0:') !== false) {
488
+ return $data;
489
+ }
490
+ // Some unserialized data cannot be re-serialized eg. SimpleXMLElements
491
+ if (is_serialized($data) && ($unserialized = @unserialize($data)) !== false) {
492
+ $data = $this->recursive_unserialize_replace($from, $to, $unserialized, true, $case_insensitive);
493
+ } elseif (is_array($data)) {
494
+ $tmp = array();
495
+ foreach ($data as $key => $value) {
496
+ $tmp[$key] = $this->recursive_unserialize_replace($from, $to, $value, false, $case_insensitive);
497
+ }
498
+
499
+ $data = $tmp;
500
+ unset($tmp);
501
+ } elseif (is_object($data)) {
502
+ $props = get_object_vars($data);
503
+
504
+ // Do a search & replace
505
+ if (empty($props['__PHP_Incomplete_Class_Name'])) {
506
+ $tmp = $data;
507
+ foreach ($props as $key => $value) {
508
+ if ($key === '' || ord($key[0]) === 0) {
509
+ continue;
510
+ }
511
+ $tmp->$key = $this->recursive_unserialize_replace($from, $to, $value, false, $case_insensitive);
512
+ }
513
+ $data = $tmp;
514
+ $tmp = '';
515
+ $props = '';
516
+ unset($tmp);
517
+ unset($props);
518
+ }
519
+ } else {
520
+ if (is_string($data)) {
521
+ if (!empty($from) && !empty($to)) {
522
+ $data = $this->str_replace($from, $to, $data, $case_insensitive);
523
+ }
524
+ }
525
+ }
526
+
527
+ if ($serialized) {
528
+ return serialize($data);
529
+ }
530
+ } catch (Exception $error) {
531
+
532
+ }
533
+
534
+ return $data;
535
+ }
536
+
537
+ /**
538
+ * Mimics the mysql_real_escape_string function. Adapted from a post by 'feedr' on php.net.
539
+ * @link http://php.net/manual/en/function.mysql-real-escape-string.php#101248
540
+ * @access public
541
+ * @param string $input The string to escape.
542
+ * @return string
543
+ */
544
+ private function mysql_escape_mimic($input)
545
+ {
546
+ if (is_array($input)) {
547
+ return array_map(__METHOD__, $input);
548
+ }
549
+ if (!empty($input) && is_string($input)) {
550
+ return str_replace(array('\\', "\0", "\n", "\r", "'", '"', "\x1a"), array('\\\\', '\\0', '\\n', '\\r', "\\'", '\\"', '\\Z'), $input);
551
+ }
552
+
553
+ return $input;
554
+ }
555
+
556
+ /**
557
+ * Return unserialized object or array
558
+ *
559
+ * @param string $serialized_string Serialized string.
560
+ * @param string $method The name of the caller method.
561
+ *
562
+ * @return mixed, false on failure
563
+ */
564
+ private static function unserialize($serialized_string)
565
+ {
566
+ if (!is_serialized($serialized_string)) {
567
+ return false;
568
+ }
569
+
570
+ $serialized_string = trim($serialized_string);
571
+ $unserialized_string = @unserialize($serialized_string);
572
+
573
+ return $unserialized_string;
574
+ }
575
+
576
+ /**
577
+ * Wrapper for str_replace
578
+ *
579
+ * @param string $from
580
+ * @param string $to
581
+ * @param string $data
582
+ * @param string|bool $case_insensitive
583
+ *
584
+ * @return string
585
+ */
586
+ private function str_replace($from, $to, $data, $case_insensitive = false)
587
+ {
588
+
589
+ // Add filter
590
+ $excludes = apply_filters('wpstg_clone_searchreplace_excl', array());
591
+
592
+ // Build pattern
593
+ $regexExclude = '';
594
+ foreach ($excludes as $exclude) {
595
+ $regexExclude .= $exclude . '(*SKIP)(FAIL)|';
596
+ }
597
+
598
+ if ('on' === $case_insensitive) {
599
+ //$data = str_ireplace( $from, $to, $data );
600
+ $data = preg_replace('#' . $regexExclude . preg_quote($from) . '#i', $to, $data);
601
+ } else {
602
+ //$data = str_replace( $from, $to, $data );
603
+ $data = preg_replace('#' . $regexExclude . preg_quote($from) . '#', $to, $data);
604
+ }
605
+
606
+ return $data;
607
+ }
608
+
609
+ /**
610
+ * Set the job
611
+ * @param string $table
612
+ */
613
+ private function setJob($table)
614
+ {
615
+ if (!empty($this->options->job->current)) {
616
+ return;
617
+ }
618
+
619
+ $this->options->job->current = $table;
620
+ $this->options->job->start = 0;
621
+ }
622
+
623
+ /**
624
+ * Start Job
625
+ * @param string $new
626
+ * @param string $old
627
+ * @return bool
628
+ */
629
+ private function startJob($new, $old)
630
+ {
631
+
632
+ if ($this->isExcludedTable($new)) {
633
+ return false;
634
+ }
635
+
636
+ // Table does not exist
637
+ $result = $this->productionDb->query("SHOW TABLES LIKE '{$old}'");
638
+ if (!$result || 0 === $result) {
639
+ return false;
640
+ }
641
+
642
+ if (0 != $this->options->job->start) {
643
+ return true;
644
+ }
645
+
646
+ $this->options->job->total = ( int )$this->productionDb->get_var("SELECT COUNT(1) FROM {$old}");
647
+
648
+ if (0 == $this->options->job->total) {
649
+ $this->finishStep();
650
+ return false;
651
+ }
652
+
653
+ return true;
654
+ }
655
+
656
+ /**
657
+ * Is table excluded from search replace processing?
658
+ * @param string $table
659
+ * @return boolean
660
+ */
661
+ private function isExcludedTable($table)
662
+ {
663
+
664
+ $customTables = apply_filters('wpstg_clone_searchreplace_tables_exclude', array());
665
+ $defaultTables = array('blogs');
666
+
667
+ $tables = array_merge($customTables, $defaultTables);
668
+
669
+ $excludedTables = array();
670
+ foreach ($tables as $key => $value) {
671
+ $excludedTables[] = $this->options->prefix . $value;
672
+ }
673
+
674
+ if (in_array($table, $excludedTables)) {
675
+ return true;
676
+ }
677
+ return false;
678
+ }
679
+
680
+ /**
681
+ * Finish the step
682
+ */
683
+ private function finishStep()
684
+ {
685
+ // This job is not finished yet
686
+ if ($this->options->job->total > $this->options->job->start) {
687
+ return false;
688
+ }
689
+
690
+ // Add it to cloned tables listing
691
+ $this->options->clonedTables[] = $this->options->tables[$this->options->currentStep];
692
+
693
+ // Reset job
694
+ $this->options->job = new \stdClass();
695
+
696
+ return true;
697
+ }
698
+
699
+ /**
700
+ * Drop table if necessary
701
+ * @param string $new
702
+ */
703
+ private function dropTable($new)
704
+ {
705
+ $old = $this->stagingDb->get_var($this->stagingDb->prepare("SHOW TABLES LIKE %s", $new));
706
+
707
+ if (!$this->shouldDropTable($new, $old)) {
708
+ return;
709
+ }
710
+
711
+ $this->log("DB Search & Replace: {$new} already exists, dropping it first");
712
+ $this->stagingDb->query("DROP TABLE {$new}");
713
+ }
714
+
715
+ /**
716
+ * Check if table needs to be dropped
717
+ * @param string $new
718
+ * @param string $old
719
+ * @return bool
720
+ */
721
+ private function shouldDropTable($new, $old)
722
+ {
723
+ return (
724
+ $old == $new &&
725
+ (
726
+ !isset($this->options->job->current) ||
727
+ !isset($this->options->job->start) ||
728
+ 0 == $this->options->job->start
729
+ )
730
+ );
731
+ }
732
+
733
+ /**
734
+ * Check if WP is installed in subdir
735
+ * @return boolean
736
+ */
737
+ private function isSubDir()
738
+ {
739
+ // Compare names without scheme to bypass cases where siteurl and home have different schemes http / https
740
+ // This is happening much more often than you would expect
741
+ $siteurl = preg_replace('#^https?://#', '', rtrim(get_option('siteurl'), '/'));
742
+ $home = preg_replace('#^https?://#', '', rtrim(get_option('home'), '/'));
743
+
744
+ if ($home !== $siteurl) {
745
+ return true;
746
+ }
747
+ return false;
748
+ }
749
+
750
+ }
Backend/Modules/Views/Forms/Settings.php CHANGED
@@ -230,12 +230,13 @@ class Settings {
230
 
231
  // Get user roles
232
  if (defined('WPSTGPRO_VERSION')) {
233
- $this->form["general"]->add(
234
- $element->setLabel(__("Access Permissions", "wp-staging"))
235
- ->setDefault( (isset( $settings->userRoles )) ? $settings->userRoles : 'administrator' )
236
- );
237
-
238
  $element = new SelectMultiple('wpstg_settings[userRoles][]', $this->getUserRoles());
 
 
 
 
 
 
239
  }
240
 
241
 
230
 
231
  // Get user roles
232
  if (defined('WPSTGPRO_VERSION')) {
 
 
 
 
 
233
  $element = new SelectMultiple('wpstg_settings[userRoles][]', $this->getUserRoles());
234
+ $this->form["general"]->add(
235
+ $element->setLabel(__("Access Permissions", "wp-staging"))
236
+ ->setDefault((isset($settings->userRoles)) ? $settings->userRoles : 'administrator')
237
+ );
238
+
239
+
240
  }
241
 
242
 
Backend/Optimizer/wp-staging-optimizer.php CHANGED
@@ -9,7 +9,7 @@
9
  * Do not use any of these methods in wp staging code base as this plugin can be missing!
10
  *
11
  * Author: René Hermenau
12
- * Version: 1.2
13
  * Author URI: https://wp-staging.com
14
  * Credit: Original version made by Delicious Brains (WP Migrate DB). Thank you guys!
15
  */
9
  * Do not use any of these methods in wp staging code base as this plugin can be missing!
10
  *
11
  * Author: René Hermenau
12
+ * Version: 1.1
13
  * Author URI: https://wp-staging.com
14
  * Credit: Original version made by Delicious Brains (WP Migrate DB). Thank you guys!
15
  */
Backend/views/clone/staging-site/index.php CHANGED
@@ -1,5 +1,5 @@
1
  <span class="wpstg-notice-alert" style="margin-top:20px;">
2
- <?php echo __("This staging site can be pushed with the WP Staging Pro Plugin installed on your production site! Open WP Staging Pro on your production site and start the pushing process from there!", "wp-staging")?>
3
  <br/>
4
  <?php echo sprintf(__("<a href='%s' target='_new'>Open WP Staging Pro on Live Site</a>"), wpstg_get_production_hostname() . '/wp-admin/admin.php?page=wpstg_clone'); ?>
5
  <br/>
1
  <span class="wpstg-notice-alert" style="margin-top:20px;">
2
+ <?php echo __("This staging site can be pushed and modified with WP Staging Pro plugin installed on your production site! Open WP Staging Pro on your production site and start the pushing process from there!", "wp-staging")?>
3
  <br/>
4
  <?php echo sprintf(__("<a href='%s' target='_new'>Open WP Staging Pro on Live Site</a>"), wpstg_get_production_hostname() . '/wp-admin/admin.php?page=wpstg_clone'); ?>
5
  <br/>
Backend/views/settings/main-settings.php CHANGED
@@ -142,7 +142,7 @@
142
  <?php _e( "<strong>Important:</strong> If CPU Load Priority is <strong>Low</strong> set a file copy limit value of 50 or higher! Otherwise file copying process takes a lot of time.", "wp-staging" ); ?>
143
  <br>
144
  <br>
145
- <strong> Default: 1 </strong>
146
  </span>
147
  </div>
148
  </td>
142
  <?php _e( "<strong>Important:</strong> If CPU Load Priority is <strong>Low</strong> set a file copy limit value of 50 or higher! Otherwise file copying process takes a lot of time.", "wp-staging" ); ?>
143
  <br>
144
  <br>
145
+ <strong> Default: 50 </strong>
146
  </span>
147
  </div>
148
  </td>
Core/Utils/Strings.php CHANGED
@@ -10,7 +10,7 @@ if( !defined( "WPINC" ) ) {
10
  class Strings {
11
 
12
  /**
13
- * Replace first occurance of certain string
14
  * @param string $search
15
  * @param string $replace
16
  * @param string $subject
10
  class Strings {
11
 
12
  /**
13
+ * Replace first occurrence of certain string
14
  * @param string $search
15
  * @param string $replace
16
  * @param string $subject
readme.txt CHANGED
@@ -1,4 +1,4 @@
1
- === WP Staging - Clone Duplicator & Migration ===
2
 
3
  Author URL: https://wordpress.org/plugins/wp-staging
4
  Plugin URL: https://wordpress.org/plugins/wp-staging
@@ -9,7 +9,7 @@ License URI: http://www.gnu.org/licenses/gpl-2.0.html
9
  Tags: staging, duplication, cloning, clone, migration, sandbox, test site, testing, backup, post, admin, administration, duplicate posts
10
  Requires at least: 3.6+
11
  Tested up to: 5.3
12
- Stable tag: 2.6.6
13
  Requires PHP: 5.3
14
 
15
  A duplicator plugin - clone/move, duplicate & migrate live websites to independent staging and development sites that are accessible​ by authorized users only.
@@ -153,6 +153,10 @@ https://wp-staging.com
153
 
154
  == Changelog ==
155
 
 
 
 
 
156
  = 2.6.6 =
157
  * Fix: Fatal error: Cannot redeclare wpstgpro_overwrite_nonce() and wpstg_overwrite_nonce() after activating pro version on top of this free one
158
  * Fix: wpdb->prepare() warning after initial cloning
1
+ === WP Staging - DB & File Duplicator & Migration ===
2
 
3
  Author URL: https://wordpress.org/plugins/wp-staging
4
  Plugin URL: https://wordpress.org/plugins/wp-staging
9
  Tags: staging, duplication, cloning, clone, migration, sandbox, test site, testing, backup, post, admin, administration, duplicate posts
10
  Requires at least: 3.6+
11
  Tested up to: 5.3
12
+ Stable tag: 2.6.7
13
  Requires PHP: 5.3
14
 
15
  A duplicator plugin - clone/move, duplicate & migrate live websites to independent staging and development sites that are accessible​ by authorized users only.
153
 
154
  == Changelog ==
155
 
156
+ = 2.6.7 =
157
+ * Fix: Update function adds duplicate string to internal urls like https://example.com/staging/staging/wp-content/*
158
+ * New: Support for WP 5.3.2
159
+
160
  = 2.6.6 =
161
  * Fix: Fatal error: Cannot redeclare wpstgpro_overwrite_nonce() and wpstg_overwrite_nonce() after activating pro version on top of this free one
162
  * Fix: wpdb->prepare() warning after initial cloning
wp-staging.php CHANGED
@@ -7,7 +7,7 @@
7
  * Author: WP-Staging
8
  * Author URI: https://wp-staging.com
9
  * Contributors: ReneHermi, ilgityildirim
10
- * Version: 2.6.6
11
  * Text Domain: wp-staging
12
  * Domain Path: /languages/
13
  *
@@ -39,12 +39,12 @@ if (!defined('WPSTG_PLUGIN_SLUG')) {
39
 
40
  // Plugin Version
41
  if (!defined('WPSTG_VERSION')) {
42
- define('WPSTG_VERSION', '2.6.6');
43
  }
44
 
45
  // Compatible up to WordPress Version
46
  if (!defined('WPSTG_COMPATIBLE')) {
47
- define('WPSTG_COMPATIBLE', '5.3.1');
48
  }
49
 
50
  // Folder Path
7
  * Author: WP-Staging
8
  * Author URI: https://wp-staging.com
9
  * Contributors: ReneHermi, ilgityildirim
10
+ * Version: 2.6.7
11
  * Text Domain: wp-staging
12
  * Domain Path: /languages/
13
  *
39
 
40
  // Plugin Version
41
  if (!defined('WPSTG_VERSION')) {
42
+ define('WPSTG_VERSION', '2.6.7');
43
  }
44
 
45
  // Compatible up to WordPress Version
46
  if (!defined('WPSTG_COMPATIBLE')) {
47
+ define('WPSTG_COMPATIBLE', '5.3.2');
48
  }
49
 
50
  // Folder Path