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 | 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 +1 -1
- Backend/Modules/Jobs/Delete.php +4 -1
- Backend/Modules/Jobs/Multisite/SearchReplace.php +318 -280
- Backend/Modules/Jobs/Multisite/SearchReplaceExternal.php +309 -272
- Backend/Modules/Jobs/SearchReplace.php +20 -8
- Backend/Modules/Jobs/SearchReplaceExternal.php +750 -714
- Backend/Modules/Views/Forms/Settings.php +6 -5
- Backend/Optimizer/wp-staging-optimizer.php +1 -1
- Backend/views/clone/staging-site/index.php +1 -1
- Backend/views/settings/main-settings.php +1 -1
- Core/Utils/Strings.php +1 -1
- readme.txt +6 -2
- wp-staging.php +3 -3
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 |
-
|
|
|
|
|
|
|
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(
|
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 |
-
|
67 |
-
$this->
|
68 |
-
$this->
|
69 |
-
$this->
|
70 |
-
$this->
|
71 |
-
$this->
|
|
|
72 |
$this->destinationHostname = $this->getDestinationHostname();
|
73 |
}
|
74 |
|
75 |
/**
|
76 |
* Get database object to interact with
|
77 |
*/
|
78 |
-
private function getStagingDB()
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
|
|
83 |
}
|
84 |
|
85 |
-
public function start()
|
|
|
86 |
// Skip job. Nothing to do
|
87 |
-
if(
|
88 |
-
$this->prepareResponse(
|
89 |
}
|
90 |
|
91 |
$this->run();
|
@@ -93,14 +97,15 @@ class SearchReplace extends JobExecutable {
|
|
93 |
// Save option, progress
|
94 |
$this->saveOptions();
|
95 |
|
96 |
-
return ( object )
|
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(
|
115 |
// Prepare response and save current progress
|
116 |
-
$this->prepareResponse(
|
117 |
$this->saveOptions();
|
118 |
return false;
|
119 |
}
|
120 |
|
121 |
// No more steps, finished
|
122 |
-
if(
|
123 |
-
$this->prepareResponse(
|
124 |
return false;
|
125 |
}
|
126 |
|
127 |
// Table is excluded
|
128 |
-
if(
|
129 |
$this->prepareResponse();
|
130 |
return true;
|
131 |
}
|
132 |
|
133 |
// Search & Replace
|
134 |
-
if(
|
135 |
// Prepare Response
|
136 |
-
$this->prepareResponse(
|
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 |
-
|
156 |
-
|
|
|
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(
|
167 |
-
|
168 |
-
$
|
|
|
169 |
$newTableName = $this->tmpPrefix . $table;
|
170 |
|
171 |
// Save current job
|
172 |
-
$this->setJob(
|
173 |
|
174 |
// Beginning of the job
|
175 |
-
if(
|
176 |
return true;
|
177 |
}
|
178 |
// Copy data
|
179 |
-
$this->startReplace(
|
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(
|
192 |
-
return trailingslashit(
|
193 |
}
|
194 |
return $this->multisiteHomeUrlWithoutScheme;
|
195 |
}
|
196 |
|
197 |
/**
|
198 |
-
* Get destination Hostname depending on
|
199 |
-
*
|
200 |
-
* @return
|
201 |
*/
|
202 |
-
private function getDestinationHostname()
|
|
|
203 |
|
204 |
-
|
205 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
206 |
}
|
207 |
|
208 |
-
|
209 |
-
|
|
|
210 |
}
|
211 |
|
212 |
-
//
|
213 |
-
|
214 |
-
|
215 |
-
|
|
|
|
|
|
|
|
|
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 |
-
|
224 |
-
$
|
|
|
225 |
|
226 |
-
if(
|
227 |
return '';
|
228 |
}
|
229 |
|
230 |
-
$dir = str_replace(
|
231 |
-
return str_replace(
|
232 |
}
|
233 |
|
234 |
/**
|
@@ -236,14 +259,15 @@ class SearchReplace extends JobExecutable {
|
|
236 |
* @param string $new
|
237 |
* @param string $old
|
238 |
*/
|
239 |
-
private function startReplace(
|
|
|
240 |
$rows = $this->options->job->start + $this->settings->querySRLimit;
|
241 |
$this->log(
|
242 |
-
|
243 |
);
|
244 |
|
245 |
// Search & Replace
|
246 |
-
$this->searchReplace(
|
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
|
256 |
* @return array
|
257 |
*/
|
258 |
-
private function get_columns(
|
|
|
259 |
$primary_key = null;
|
260 |
-
$columns
|
261 |
-
$fields
|
262 |
-
if(
|
263 |
-
foreach (
|
264 |
$columns[] = $column->Field;
|
265 |
-
if(
|
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
|
283 |
-
* @param
|
284 |
-
* @param
|
285 |
* @return array
|
286 |
*/
|
287 |
private function searchReplace($table, $page, $args)
|
288 |
{
|
289 |
|
290 |
-
|
291 |
-
|
292 |
-
|
293 |
-
|
294 |
|
295 |
$table = esc_sql($table);
|
296 |
|
297 |
$args['search_for'] = array(
|
298 |
-
|
299 |
-
'\/\/'.str_replace('/', '\/', $this->sourceHostname), // Escaped \/ used by revslider and several visual editors
|
300 |
-
'//'
|
301 |
ABSPATH
|
302 |
-
|
303 |
|
304 |
-
|
305 |
-
'%2F%2F'.str_replace('/', '%2F', $this->destinationHostname),
|
306 |
-
'\/\/'.str_replace('/', '\/', $this->destinationHostname),
|
307 |
-
|
308 |
$this->options->destinationDir
|
309 |
-
|
310 |
|
311 |
-
|
312 |
-
|
313 |
|
314 |
|
315 |
-
|
316 |
-
|
317 |
-
|
318 |
-
|
319 |
|
320 |
|
321 |
-
|
322 |
-
|
323 |
|
324 |
// Get columns and primary keys
|
325 |
-
|
326 |
-
|
327 |
-
|
328 |
-
|
329 |
-
|
330 |
-
|
331 |
-
|
332 |
-
|
333 |
-
|
334 |
-
|
335 |
-
|
336 |
-
|
337 |
-
|
338 |
-
|
339 |
-
|
340 |
-
|
341 |
-
|
342 |
-
|
343 |
-
|
344 |
-
|
345 |
-
|
346 |
-
|
347 |
-
|
348 |
-
|
349 |
-
|
350 |
-
|
351 |
'upload_path',
|
352 |
-
|
353 |
-
|
354 |
-
|
355 |
-
|
356 |
-
|
357 |
-
|
358 |
-
|
359 |
|
360 |
-
|
361 |
|
362 |
// Go through the table rows
|
363 |
-
|
364 |
-
|
365 |
-
|
366 |
-
|
367 |
-
|
368 |
|
369 |
// Skip rows
|
370 |
-
|
371 |
-
|
372 |
-
|
373 |
|
374 |
// Skip transients (There can be thousands of them. Save memory and increase performance)
|
375 |
-
|
376 |
-
|
377 |
-
|
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 |
-
|
381 |
-
|
382 |
-
|
383 |
|
384 |
-
|
385 |
-
|
386 |
|
387 |
-
|
388 |
|
389 |
// Skip column larger than 5MB
|
390 |
-
|
391 |
-
|
392 |
-
|
393 |
-
|
394 |
|
395 |
// Skip primary key column
|
396 |
-
|
397 |
-
|
398 |
-
|
399 |
-
|
400 |
|
401 |
-
|
402 |
-
|
403 |
-
|
404 |
-
|
405 |
|
406 |
|
407 |
-
|
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 |
-
|
411 |
-
|
412 |
unset($replace, $i);
|
413 |
|
414 |
-
|
415 |
-
|
416 |
-
$update_sql[] = $column.' = "'
|
417 |
-
$upd
|
418 |
-
|
419 |
-
|
420 |
-
|
421 |
-
|
422 |
-
|
423 |
-
|
424 |
-
|
425 |
-
|
426 |
-
|
427 |
-
|
428 |
-
|
429 |
-
|
430 |
-
|
431 |
-
|
432 |
-
|
433 |
-
|
434 |
-
|
435 |
-
|
436 |
-
|
437 |
-
|
438 |
-
|
439 |
-
|
440 |
-
|
441 |
-
|
442 |
-
|
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
|
454 |
|
455 |
-
if(
|
456 |
// Since WP 3.5
|
457 |
$path = $blogId > 1 ?
|
458 |
-
|
459 |
-
|
460 |
} else {
|
461 |
// old blog structure
|
462 |
$path = $blogId > 1 ?
|
463 |
-
|
464 |
-
|
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
|
487 |
-
* @param
|
488 |
-
* @param
|
489 |
-
* @param
|
490 |
-
* @param
|
491 |
*
|
492 |
-
* @return string|array
|
493 |
*/
|
494 |
-
private function recursive_unserialize_replace(
|
|
|
495 |
try {
|
496 |
// PDO instances can not be serialized or unserialized
|
497 |
-
if(
|
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(
|
504 |
return $data;
|
505 |
}
|
506 |
// Some unserialized data cannot be re-serialized eg. SimpleXMLElements
|
507 |
-
if(
|
508 |
-
$data = $this->recursive_unserialize_replace(
|
509 |
-
} elseif(
|
510 |
$tmp = array();
|
511 |
-
foreach (
|
512 |
-
$tmp[$key] = $this->recursive_unserialize_replace(
|
513 |
}
|
514 |
|
515 |
$data = $tmp;
|
516 |
-
unset(
|
517 |
-
} elseif(
|
518 |
-
$props = get_object_vars(
|
519 |
|
520 |
// Do a search & replace
|
521 |
-
if(
|
522 |
$tmp = $data;
|
523 |
-
foreach (
|
524 |
-
if(
|
525 |
continue;
|
526 |
}
|
527 |
-
$tmp->$key = $this->recursive_unserialize_replace(
|
528 |
}
|
529 |
-
$data
|
530 |
-
$tmp
|
531 |
$props = '';
|
532 |
-
unset(
|
533 |
-
unset(
|
534 |
}
|
535 |
} else {
|
536 |
-
if(
|
537 |
-
if(
|
538 |
-
$data = $this->str_replace(
|
539 |
}
|
540 |
}
|
541 |
}
|
542 |
|
543 |
-
if(
|
544 |
-
return serialize(
|
545 |
}
|
546 |
-
} catch (
|
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
|
558 |
* @return string
|
559 |
*/
|
560 |
-
private function mysql_escape_mimic(
|
561 |
-
|
562 |
-
|
|
|
563 |
}
|
564 |
-
if(
|
565 |
-
return str_replace(
|
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
|
576 |
*
|
577 |
* @return mixed, false on failure
|
578 |
*/
|
579 |
-
private static function unserialize(
|
580 |
-
|
|
|
581 |
return false;
|
582 |
}
|
583 |
|
584 |
-
$serialized_string
|
585 |
-
$unserialized_string = @unserialize(
|
586 |
|
587 |
return $unserialized_string;
|
588 |
}
|
@@ -597,23 +627,24 @@ class SearchReplace extends JobExecutable {
|
|
597 |
*
|
598 |
* @return string
|
599 |
*/
|
600 |
-
private function str_replace(
|
|
|
601 |
|
602 |
// Add filter
|
603 |
-
$excludes = apply_filters(
|
604 |
|
605 |
// Build pattern
|
606 |
$regexExclude = '';
|
607 |
-
foreach (
|
608 |
$regexExclude .= $exclude . '(*SKIP)(FAIL)|';
|
609 |
}
|
610 |
|
611 |
-
if(
|
612 |
//$data = str_ireplace( $from, $to, $data );
|
613 |
-
$data = preg_replace(
|
614 |
} else {
|
615 |
//$data = str_replace( $from, $to, $data );
|
616 |
-
$data = preg_replace(
|
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(
|
627 |
-
|
|
|
628 |
return;
|
629 |
}
|
630 |
|
631 |
$this->options->job->current = $table;
|
632 |
-
$this->options->job->start
|
633 |
}
|
634 |
|
635 |
/**
|
@@ -638,21 +670,22 @@ class SearchReplace extends JobExecutable {
|
|
638 |
* @param string $old
|
639 |
* @return bool
|
640 |
*/
|
641 |
-
private function startJob(
|
|
|
642 |
|
643 |
-
if(
|
644 |
return false;
|
645 |
}
|
646 |
|
647 |
// Table does not exist
|
648 |
-
$result = $this->productionDb->query(
|
649 |
-
if(
|
650 |
return false;
|
651 |
}
|
652 |
|
653 |
-
$this->options->job->total = ( int )
|
654 |
|
655 |
-
if(
|
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(
|
|
|
669 |
|
670 |
-
$customTables
|
671 |
$defaultTables = array('blogs');
|
672 |
|
673 |
-
$tables = array_merge(
|
674 |
|
675 |
$excludedTables = array();
|
676 |
-
foreach (
|
677 |
$excludedTables[] = $this->options->prefix . $value;
|
678 |
}
|
679 |
|
680 |
-
if(
|
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(
|
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(
|
709 |
-
|
|
|
710 |
|
711 |
-
if(
|
712 |
return;
|
713 |
}
|
714 |
|
715 |
-
$this->log(
|
716 |
-
$this->productionDb->query(
|
717 |
}
|
718 |
|
719 |
/**
|
@@ -722,28 +758,30 @@ class SearchReplace extends JobExecutable {
|
|
722 |
* @param string $old
|
723 |
* @return bool
|
724 |
*/
|
725 |
-
private function shouldDropTable(
|
|
|
726 |
return (
|
727 |
-
|
728 |
-
|
729 |
-
!isset(
|
730 |
-
!isset(
|
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(
|
744 |
-
$home
|
745 |
|
746 |
-
if(
|
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(
|
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 |
-
|
66 |
-
$this->
|
67 |
-
$this->
|
68 |
-
$this->
|
69 |
-
$this->
|
70 |
-
$this->
|
|
|
71 |
$this->destinationHostname = $this->getDestinationHostname();
|
72 |
}
|
73 |
|
74 |
/**
|
75 |
* Get database object to interact with
|
76 |
*/
|
77 |
-
private function getStagingDB()
|
78 |
-
|
|
|
79 |
}
|
80 |
|
81 |
-
public function start()
|
|
|
82 |
// Skip job. Nothing to do
|
83 |
-
if(
|
84 |
-
$this->prepareResponse(
|
85 |
}
|
86 |
|
87 |
$this->run();
|
@@ -89,14 +93,15 @@ class SearchReplaceExternal extends JobExecutable {
|
|
89 |
// Save option, progress
|
90 |
$this->saveOptions();
|
91 |
|
92 |
-
return ( object )
|
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(
|
111 |
// Prepare response and save current progress
|
112 |
-
$this->prepareResponse(
|
113 |
$this->saveOptions();
|
114 |
return false;
|
115 |
}
|
116 |
|
117 |
// No more steps, finished
|
118 |
-
if(
|
119 |
-
$this->prepareResponse(
|
120 |
return false;
|
121 |
}
|
122 |
|
123 |
// Table is excluded
|
124 |
-
if(
|
125 |
$this->prepareResponse();
|
126 |
return true;
|
127 |
}
|
128 |
|
129 |
// Search & Replace
|
130 |
if (!$this->updateTable($this->options->tables[$this->options->currentStep])) {
|
131 |
-
|
132 |
-
|
133 |
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
|
138 |
|
139 |
-
|
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(
|
152 |
-
|
153 |
-
$
|
|
|
154 |
$newTableName = $this->tmpPrefix . $table;
|
155 |
|
156 |
// Save current job
|
157 |
-
$this->setJob(
|
158 |
|
159 |
// Beginning of the job
|
160 |
-
if(
|
161 |
return true;
|
162 |
}
|
163 |
// Copy data
|
164 |
-
$this->startReplace(
|
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(
|
177 |
-
return trailingslashit(
|
178 |
}
|
179 |
return $this->multisiteHomeUrlWithoutScheme;
|
180 |
}
|
181 |
|
182 |
/**
|
183 |
-
* Get destination Hostname depending on
|
184 |
* Retun host name without scheme
|
185 |
-
* @return
|
186 |
*/
|
187 |
-
private function getDestinationHostname()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
188 |
|
189 |
-
|
190 |
-
|
|
|
191 |
}
|
192 |
|
193 |
-
|
194 |
-
|
|
|
195 |
}
|
196 |
|
197 |
-
//
|
198 |
-
$multisitePath = defined(
|
199 |
-
|
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 |
-
|
209 |
-
$
|
|
|
210 |
|
211 |
-
if(
|
212 |
return '';
|
213 |
}
|
214 |
|
215 |
-
$dir = str_replace(
|
216 |
-
return str_replace(
|
217 |
}
|
218 |
|
219 |
/**
|
@@ -221,14 +243,15 @@ class SearchReplaceExternal extends JobExecutable {
|
|
221 |
* @param string $new
|
222 |
* @param string $old
|
223 |
*/
|
224 |
-
private function startReplace(
|
|
|
225 |
$rows = $this->options->job->start + $this->settings->querySRLimit;
|
226 |
$this->log(
|
227 |
-
|
228 |
);
|
229 |
|
230 |
// Search & Replace
|
231 |
-
$this->searchReplace(
|
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
|
241 |
* @return array
|
242 |
*/
|
243 |
-
private function get_columns(
|
|
|
244 |
$primary_key = null;
|
245 |
-
$columns
|
246 |
-
$fields
|
247 |
-
if(
|
248 |
-
foreach (
|
249 |
$columns[] = $column->Field;
|
250 |
-
if(
|
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
|
268 |
-
* @param
|
269 |
-
* @param
|
270 |
* @return array
|
271 |
*/
|
272 |
private function searchReplace($table, $page, $args)
|
273 |
{
|
274 |
|
275 |
-
|
276 |
$this->log("DB Search & Replace: Skip {$table}", \WPStaging\Utils\Logger::TYPE_INFO);
|
277 |
-
|
278 |
-
|
279 |
|
280 |
$table = esc_sql($table);
|
281 |
|
282 |
$args['search_for'] = array(
|
283 |
-
|
284 |
-
'\/\/'.str_replace('/', '\/', $this->sourceHostname), // Escaped \/ used by revslider and several visual editors
|
285 |
-
'//'
|
286 |
ABSPATH
|
287 |
-
|
288 |
|
289 |
-
|
290 |
-
'%2F%2F'.str_replace('/', '%2F', $this->destinationHostname),
|
291 |
-
'\/\/'.str_replace('/', '\/', $this->destinationHostname),
|
292 |
-
|
293 |
$this->options->destinationDir
|
294 |
-
|
295 |
|
296 |
-
|
297 |
-
|
298 |
|
299 |
|
300 |
-
|
301 |
-
|
302 |
-
|
303 |
-
|
304 |
|
305 |
|
306 |
-
|
307 |
-
|
308 |
|
309 |
// Get columns and primary keys
|
310 |
-
|
311 |
|
312 |
-
|
313 |
-
|
314 |
-
|
315 |
-
|
316 |
-
|
317 |
-
|
318 |
-
|
319 |
|
320 |
$current_row = 0;
|
321 |
-
$start
|
322 |
-
$end
|
323 |
-
|
324 |
-
$data = $this->stagingDb->get_results(
|
325 |
-
|
326 |
-
|
327 |
-
|
328 |
-
|
329 |
-
|
330 |
-
|
331 |
-
|
332 |
-
|
333 |
-
|
334 |
-
|
335 |
-
|
336 |
-
|
337 |
'upload_path',
|
338 |
-
|
339 |
-
|
340 |
-
|
341 |
-
|
342 |
-
|
343 |
-
|
344 |
-
|
345 |
|
346 |
-
|
347 |
|
348 |
// Go through the table rows
|
349 |
-
|
350 |
-
|
351 |
-
|
352 |
-
|
353 |
-
|
354 |
|
355 |
// Skip rows
|
356 |
-
|
357 |
-
|
358 |
-
|
359 |
|
360 |
// Skip transients (There can be thousands of them. Save memory and increase performance)
|
361 |
-
|
362 |
-
|
363 |
-
|
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 |
-
|
367 |
-
|
368 |
-
|
369 |
|
370 |
-
|
371 |
-
|
372 |
|
373 |
-
|
374 |
|
375 |
// Skip column larger than 5MB
|
376 |
-
|
377 |
-
|
378 |
-
|
379 |
-
|
380 |
|
381 |
// Skip primary key column
|
382 |
-
|
383 |
-
|
384 |
-
|
385 |
-
|
386 |
|
387 |
-
|
388 |
-
|
389 |
-
|
390 |
-
|
391 |
|
392 |
|
393 |
-
|
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 |
-
|
397 |
-
|
398 |
unset($replace, $i);
|
399 |
|
400 |
-
|
401 |
-
|
402 |
-
$update_sql[] = $column.' = "'
|
403 |
-
$upd
|
404 |
-
|
405 |
-
|
406 |
-
|
407 |
-
|
408 |
-
|
409 |
-
|
410 |
-
|
411 |
-
|
412 |
-
|
413 |
-
|
414 |
-
|
415 |
-
|
416 |
-
|
417 |
-
|
418 |
-
|
419 |
-
|
420 |
-
|
421 |
-
|
422 |
-
|
423 |
-
|
424 |
-
|
425 |
-
|
426 |
-
|
427 |
-
|
428 |
-
|
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
|
440 |
|
441 |
-
if(
|
442 |
// Since WP 3.5
|
443 |
$path = $blogId > 1 ?
|
444 |
-
|
445 |
-
|
446 |
} else {
|
447 |
// old blog structure
|
448 |
$path = $blogId > 1 ?
|
449 |
-
|
450 |
-
|
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
|
473 |
-
* @param
|
474 |
-
* @param
|
475 |
-
* @param
|
476 |
-
* @param
|
477 |
*
|
478 |
-
* @return string|array
|
479 |
*/
|
480 |
-
private function recursive_unserialize_replace(
|
|
|
481 |
try {
|
482 |
// PDO instances can not be serialized or unserialized
|
483 |
-
if(
|
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(
|
490 |
return $data;
|
491 |
}
|
492 |
// Some unserialized data cannot be re-serialized eg. SimpleXMLElements
|
493 |
-
if(
|
494 |
-
$data = $this->recursive_unserialize_replace(
|
495 |
-
} elseif(
|
496 |
$tmp = array();
|
497 |
-
foreach (
|
498 |
-
$tmp[$key] = $this->recursive_unserialize_replace(
|
499 |
}
|
500 |
|
501 |
$data = $tmp;
|
502 |
-
unset(
|
503 |
-
} elseif(
|
504 |
-
$props = get_object_vars(
|
505 |
|
506 |
// Do a search & replace
|
507 |
-
if(
|
508 |
$tmp = $data;
|
509 |
-
foreach (
|
510 |
-
if(
|
511 |
continue;
|
512 |
}
|
513 |
-
$tmp->$key = $this->recursive_unserialize_replace(
|
514 |
}
|
515 |
-
$data
|
516 |
-
$tmp
|
517 |
$props = '';
|
518 |
-
unset(
|
519 |
-
unset(
|
520 |
}
|
521 |
} else {
|
522 |
-
if(
|
523 |
-
if(
|
524 |
-
$data = $this->str_replace(
|
525 |
}
|
526 |
}
|
527 |
}
|
528 |
|
529 |
-
if(
|
530 |
-
return serialize(
|
531 |
}
|
532 |
-
} catch (
|
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
|
544 |
* @return string
|
545 |
*/
|
546 |
-
private function mysql_escape_mimic(
|
547 |
-
|
548 |
-
|
|
|
549 |
}
|
550 |
-
if(
|
551 |
-
return str_replace(
|
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
|
562 |
*
|
563 |
* @return mixed, false on failure
|
564 |
*/
|
565 |
-
private static function unserialize(
|
566 |
-
|
|
|
567 |
return false;
|
568 |
}
|
569 |
|
570 |
-
$serialized_string
|
571 |
-
$unserialized_string = @unserialize(
|
572 |
|
573 |
return $unserialized_string;
|
574 |
}
|
@@ -583,23 +612,24 @@ class SearchReplaceExternal extends JobExecutable {
|
|
583 |
*
|
584 |
* @return string
|
585 |
*/
|
586 |
-
private function str_replace(
|
|
|
587 |
|
588 |
// Add filter
|
589 |
-
$excludes = apply_filters(
|
590 |
|
591 |
// Build pattern
|
592 |
$regexExclude = '';
|
593 |
-
foreach (
|
594 |
$regexExclude .= $exclude . '(*SKIP)(FAIL)|';
|
595 |
}
|
596 |
|
597 |
-
if(
|
598 |
//$data = str_ireplace( $from, $to, $data );
|
599 |
-
$data = preg_replace(
|
600 |
} else {
|
601 |
//$data = str_replace( $from, $to, $data );
|
602 |
-
$data = preg_replace(
|
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(
|
613 |
-
|
|
|
614 |
return;
|
615 |
}
|
616 |
|
617 |
$this->options->job->current = $table;
|
618 |
-
$this->options->job->start
|
619 |
}
|
620 |
|
621 |
/**
|
@@ -624,25 +655,26 @@ class SearchReplaceExternal extends JobExecutable {
|
|
624 |
* @param string $old
|
625 |
* @return bool
|
626 |
*/
|
627 |
-
private function startJob(
|
|
|
628 |
|
629 |
-
if(
|
630 |
return false;
|
631 |
}
|
632 |
|
633 |
// Table does not exist
|
634 |
-
$result = $this->productionDb->query(
|
635 |
-
if(
|
636 |
return false;
|
637 |
}
|
638 |
|
639 |
-
if(
|
640 |
return true;
|
641 |
}
|
642 |
|
643 |
-
$this->options->job->total = ( int )
|
644 |
|
645 |
-
if(
|
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(
|
|
|
659 |
|
660 |
-
$customTables
|
661 |
$defaultTables = array('blogs');
|
662 |
|
663 |
-
$tables = array_merge(
|
664 |
|
665 |
$excludedTables = array();
|
666 |
-
foreach (
|
667 |
$excludedTables[] = $this->options->prefix . $value;
|
668 |
}
|
669 |
|
670 |
-
if(
|
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(
|
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(
|
699 |
-
|
|
|
700 |
|
701 |
-
if(
|
702 |
return;
|
703 |
}
|
704 |
|
705 |
-
$this->log(
|
706 |
-
$this->stagingDb->query(
|
707 |
}
|
708 |
|
709 |
/**
|
@@ -712,28 +747,30 @@ class SearchReplaceExternal extends JobExecutable {
|
|
712 |
* @param string $old
|
713 |
* @return bool
|
714 |
*/
|
715 |
-
private function shouldDropTable(
|
|
|
716 |
return (
|
717 |
-
|
718 |
-
|
719 |
-
!isset(
|
720 |
-
!isset(
|
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(
|
734 |
-
$home
|
735 |
|
736 |
-
if(
|
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)
|
84 |
}
|
85 |
|
86 |
/**
|
@@ -189,22 +189,34 @@ class SearchReplace extends JobExecutable
|
|
189 |
}
|
190 |
|
191 |
/**
|
192 |
-
* Get destination Hostname depending on
|
193 |
-
* @return
|
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 |
-
|
202 |
}
|
203 |
|
|
|
204 |
if ($this->isSubDir()) {
|
205 |
-
$
|
206 |
}
|
207 |
-
|
|
|
|
|
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(
|
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 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
*
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
*
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
*
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
*
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
$this->
|
67 |
-
$this->
|
68 |
-
$this->
|
69 |
-
$this->
|
70 |
-
$this->
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
$this->
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
$this->
|
122 |
-
return false;
|
123 |
-
}
|
124 |
-
|
125 |
-
//
|
126 |
-
if
|
127 |
-
$this->prepareResponse();
|
128 |
-
return
|
129 |
-
}
|
130 |
-
|
131 |
-
//
|
132 |
-
if (
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
//
|
166 |
-
$this->
|
167 |
-
|
168 |
-
//
|
169 |
-
|
170 |
-
|
171 |
-
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
-
|
189 |
-
|
190 |
-
|
191 |
-
|
192 |
-
|
193 |
-
|
194 |
-
|
195 |
-
|
196 |
-
|
197 |
-
|
198 |
-
|
199 |
-
|
200 |
-
|
201 |
-
|
202 |
-
|
203 |
-
|
204 |
-
|
205 |
-
|
206 |
-
|
207 |
-
|
208 |
-
|
209 |
-
|
210 |
-
|
211 |
-
|
212 |
-
|
213 |
-
if
|
214 |
-
return
|
215 |
-
}
|
216 |
-
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
|
221 |
-
|
222 |
-
|
223 |
-
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
|
229 |
-
|
230 |
-
|
231 |
-
|
232 |
-
|
233 |
-
$
|
234 |
-
|
235 |
-
|
236 |
-
$
|
237 |
-
|
238 |
-
|
239 |
-
|
240 |
-
|
241 |
-
|
242 |
-
|
243 |
-
|
244 |
-
|
245 |
-
|
246 |
-
|
247 |
-
|
248 |
-
|
249 |
-
|
250 |
-
|
251 |
-
|
252 |
-
|
253 |
-
|
254 |
-
|
255 |
-
|
256 |
-
|
257 |
-
|
258 |
-
|
259 |
-
|
260 |
-
|
261 |
-
|
262 |
-
|
263 |
-
|
264 |
-
*
|
265 |
-
*
|
266 |
-
* @
|
267 |
-
*
|
268 |
-
|
269 |
-
|
270 |
-
|
271 |
-
|
272 |
-
|
273 |
-
|
274 |
-
|
275 |
-
|
276 |
-
|
277 |
-
|
278 |
-
|
279 |
-
|
280 |
-
|
281 |
-
}
|
282 |
-
|
283 |
-
|
284 |
-
|
285 |
-
|
286 |
-
|
287 |
-
|
288 |
-
|
289 |
-
|
290 |
-
|
291 |
-
|
292 |
-
|
293 |
-
|
294 |
-
|
295 |
-
|
296 |
-
|
297 |
-
|
298 |
-
|
299 |
-
|
300 |
-
|
301 |
-
|
302 |
-
|
303 |
-
|
304 |
-
|
305 |
-
|
306 |
-
|
307 |
-
|
308 |
-
|
309 |
-
|
310 |
-
$args =
|
311 |
-
|
312 |
-
|
313 |
-
|
314 |
-
|
315 |
-
|
316 |
-
|
317 |
-
|
318 |
-
|
319 |
-
|
320 |
-
|
321 |
-
|
322 |
-
|
323 |
-
|
324 |
-
$
|
325 |
-
$
|
326 |
-
|
327 |
-
|
328 |
-
|
329 |
-
|
330 |
-
|
331 |
-
$
|
332 |
-
|
333 |
-
|
334 |
-
|
335 |
-
|
336 |
-
|
337 |
-
|
338 |
-
|
339 |
-
|
340 |
-
|
341 |
-
|
342 |
-
|
343 |
-
|
344 |
-
|
345 |
-
|
346 |
-
|
347 |
-
|
348 |
-
|
349 |
-
$
|
350 |
-
|
351 |
-
|
352 |
-
|
353 |
-
|
354 |
-
|
355 |
-
|
356 |
-
|
357 |
-
|
358 |
-
|
359 |
-
|
360 |
-
|
361 |
-
|
362 |
-
|
363 |
-
|
364 |
-
|
365 |
-
|
366 |
-
|
367 |
-
|
368 |
-
|
369 |
-
|
370 |
-
|
371 |
-
|
372 |
-
|
373 |
-
|
374 |
-
|
375 |
-
|
376 |
-
|
377 |
-
|
378 |
-
|
379 |
-
|
380 |
-
|
381 |
-
|
382 |
-
|
383 |
-
|
384 |
-
|
385 |
-
|
386 |
-
|
387 |
-
|
388 |
-
|
389 |
-
|
390 |
-
|
391 |
-
|
392 |
-
|
393 |
-
|
394 |
-
|
395 |
-
|
396 |
-
|
397 |
-
|
398 |
-
|
399 |
-
|
400 |
-
|
401 |
-
|
402 |
-
|
403 |
-
//
|
404 |
-
|
405 |
-
|
406 |
-
|
407 |
-
}
|
408 |
-
|
409 |
-
|
410 |
-
|
411 |
-
|
412 |
-
|
413 |
-
|
414 |
-
|
415 |
-
|
416 |
-
$
|
417 |
-
|
418 |
-
|
419 |
-
|
420 |
-
|
421 |
-
|
422 |
-
|
423 |
-
|
424 |
-
|
425 |
-
|
426 |
-
|
427 |
-
|
428 |
-
|
429 |
-
|
430 |
-
|
431 |
-
|
432 |
-
|
433 |
-
|
434 |
-
|
435 |
-
|
436 |
-
|
437 |
-
|
438 |
-
|
439 |
-
|
440 |
-
|
441 |
-
|
442 |
-
|
443 |
-
|
444 |
-
|
445 |
-
|
446 |
-
|
447 |
-
|
448 |
-
|
449 |
-
|
450 |
-
|
451 |
-
|
452 |
-
|
453 |
-
|
454 |
-
|
455 |
-
|
456 |
-
|
457 |
-
|
458 |
-
|
459 |
-
|
460 |
-
|
461 |
-
|
462 |
-
|
463 |
-
|
464 |
-
|
465 |
-
|
466 |
-
|
467 |
-
|
468 |
-
|
469 |
-
|
470 |
-
|
471 |
-
|
472 |
-
|
473 |
-
|
474 |
-
|
475 |
-
|
476 |
-
|
477 |
-
|
478 |
-
|
479 |
-
|
480 |
-
|
481 |
-
|
482 |
-
|
483 |
-
|
484 |
-
|
485 |
-
|
486 |
-
|
487 |
-
|
488 |
-
|
489 |
-
|
490 |
-
|
491 |
-
|
492 |
-
|
493 |
-
}
|
494 |
-
|
495 |
-
|
496 |
-
|
497 |
-
|
498 |
-
|
499 |
-
|
500 |
-
|
501 |
-
|
502 |
-
|
503 |
-
|
504 |
-
|
505 |
-
|
506 |
-
|
507 |
-
|
508 |
-
|
509 |
-
|
510 |
-
|
511 |
-
|
512 |
-
|
513 |
-
|
514 |
-
|
515 |
-
|
516 |
-
|
517 |
-
|
518 |
-
|
519 |
-
|
520 |
-
|
521 |
-
|
522 |
-
|
523 |
-
|
524 |
-
|
525 |
-
|
526 |
-
|
527 |
-
|
528 |
-
|
529 |
-
|
530 |
-
|
531 |
-
|
532 |
-
|
533 |
-
|
534 |
-
|
535 |
-
|
536 |
-
|
537 |
-
|
538 |
-
|
539 |
-
|
540 |
-
|
541 |
-
|
542 |
-
|
543 |
-
|
544 |
-
|
545 |
-
|
546 |
-
|
547 |
-
|
548 |
-
|
549 |
-
|
550 |
-
|
551 |
-
|
552 |
-
|
553 |
-
|
554 |
-
|
555 |
-
|
556 |
-
|
557 |
-
|
558 |
-
|
559 |
-
|
560 |
-
|
561 |
-
|
562 |
-
|
563 |
-
|
564 |
-
|
565 |
-
|
566 |
-
|
567 |
-
|
568 |
-
|
569 |
-
|
570 |
-
|
571 |
-
|
572 |
-
|
573 |
-
|
574 |
-
|
575 |
-
|
576 |
-
|
577 |
-
|
578 |
-
|
579 |
-
|
580 |
-
|
581 |
-
*
|
582 |
-
* @param string $
|
583 |
-
|
584 |
-
|
585 |
-
|
586 |
-
|
587 |
-
|
588 |
-
|
589 |
-
|
590 |
-
$
|
591 |
-
|
592 |
-
|
593 |
-
|
594 |
-
|
595 |
-
|
596 |
-
|
597 |
-
|
598 |
-
|
599 |
-
|
600 |
-
|
601 |
-
|
602 |
-
|
603 |
-
|
604 |
-
|
605 |
-
|
606 |
-
|
607 |
-
|
608 |
-
|
609 |
-
|
610 |
-
|
611 |
-
|
612 |
-
|
613 |
-
|
614 |
-
|
615 |
-
$this->options->job->
|
616 |
-
|
617 |
-
|
618 |
-
|
619 |
-
|
620 |
-
|
621 |
-
|
622 |
-
|
623 |
-
|
624 |
-
|
625 |
-
|
626 |
-
*
|
627 |
-
* @
|
628 |
-
|
629 |
-
|
630 |
-
|
631 |
-
|
632 |
-
|
633 |
-
|
634 |
-
|
635 |
-
|
636 |
-
|
637 |
-
$
|
638 |
-
|
639 |
-
|
640 |
-
}
|
641 |
-
|
642 |
-
if
|
643 |
-
return true;
|
644 |
-
}
|
645 |
-
|
646 |
-
|
647 |
-
|
648 |
-
|
649 |
-
|
650 |
-
|
651 |
-
|
652 |
-
|
653 |
-
|
654 |
-
|
655 |
-
|
656 |
-
|
657 |
-
|
658 |
-
|
659 |
-
|
660 |
-
|
661 |
-
|
662 |
-
|
663 |
-
|
664 |
-
|
665 |
-
|
666 |
-
|
667 |
-
|
668 |
-
|
669 |
-
|
670 |
-
|
671 |
-
|
672 |
-
|
673 |
-
|
674 |
-
|
675 |
-
|
676 |
-
|
677 |
-
|
678 |
-
|
679 |
-
|
680 |
-
|
681 |
-
|
682 |
-
|
683 |
-
|
684 |
-
|
685 |
-
|
686 |
-
|
687 |
-
|
688 |
-
|
689 |
-
|
690 |
-
|
691 |
-
|
692 |
-
|
693 |
-
|
694 |
-
|
695 |
-
|
696 |
-
|
697 |
-
|
698 |
-
|
699 |
-
|
700 |
-
*
|
701 |
-
|
702 |
-
|
703 |
-
|
704 |
-
|
705 |
-
$
|
706 |
-
|
707 |
-
|
708 |
-
|
709 |
-
|
710 |
-
|
711 |
-
|
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.
|
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
|
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:
|
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
|
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 -
|
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.
|
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.
|
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.
|
43 |
}
|
44 |
|
45 |
// Compatible up to WordPress Version
|
46 |
if (!defined('WPSTG_COMPATIBLE')) {
|
47 |
-
define('WPSTG_COMPATIBLE', '5.3.
|
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
|