PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backups, Restore, Migration & Clone / 4.11.0
WP STAGING – WordPress Backups, Restore, Migration & Clone v4.11.0
4.11.0 4.10.0 4.9.5 4.9.4 4.9.3 4.9.2 4.9.1 4.9.0 4.8.1 trunk 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.1.0 3.1.1 3.1.2 3.1.3 3.1.4 3.10.0 3.2.0 3.3.1 3.3.2 3.3.3 3.4.1 3.4.3 3.5.0 3.6.0 3.7.1 3.8.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.8.6 3.8.7 3.9.0 3.9.1 3.9.2 3.9.3 3.9.4 4.0.0 4.1.0 4.1.1 4.1.2 4.1.3 4.1.4 4.2.0 4.2.1 4.3.0 4.3.1 4.3.2 4.4.0 4.5.0 4.6.0 4.7.0 4.7.1 4.7.2 4.7.3 4.8.0
wp-staging / Backend / Modules / Jobs / SearchReplace.php
wp-staging / Backend / Modules / Jobs Last commit date
Cleaners 1 day ago Exceptions 1 day ago Cancel.php 1 day ago CancelUpdate.php 1 day ago Cloning.php 1 day ago CloningProcess.php 1 day ago Data.php 1 day ago Database.php 1 day ago Delete.php 1 day ago Directories.php 1 day ago Files.php 1 day ago Finish.php 1 day ago Job.php 1 day ago JobExecutable.php 1 day ago Logs.php 1 day ago PreserveDataFirstStep.php 1 day ago PreserveDataSecondStep.php 1 day ago ProcessLock.php 1 day ago Scan.php 1 day ago SearchReplace.php 1 day ago TotalStepsAreNumberOfTables.php 1 day ago Updating.php 1 day ago
SearchReplace.php
622 lines
1 <?php
2
3 namespace WPStaging\Backend\Modules\Jobs;
4
5 use stdClass;
6 use wpdb;
7 use WPStaging\Core\WPStaging;
8 use WPStaging\Core\Utils\Logger;
9 use WPStaging\Core\Utils\Multisite;
10 use WPStaging\Framework\Facades\Hooks;
11 use WPStaging\Framework\SiteInfo;
12 use WPStaging\Framework\Traits\DatabaseSearchReplaceTrait;
13 use WPStaging\Framework\Traits\DbRowsGeneratorTrait;
14 use WPStaging\Framework\Utils\Strings;
15 use WPStaging\Framework\Utils\Escape;
16
17
18
19
20
21
22
23
24
25
26
27 class SearchReplace extends CloningProcess
28 {
29 use TotalStepsAreNumberOfTables;
30 use DbRowsGeneratorTrait;
31 use DatabaseSearchReplaceTrait;
32
33
34 const FILTER_CLONE_SEARCH_REPLACE_EXCLUDED_ROWS = 'wpstg_clone_searchreplace_excl_rows';
35
36
37 const FILTER_CLONE_SEARCH_REPLACE_EXCLUDED = 'wpstg_clone_searchreplace_excl';
38
39
40 const FILTER_CLONE_SEARCH_REPLACE_PARAMS = 'wpstg_clone_searchreplace_params';
41
42
43
44
45
46
47 protected $maxFailedAttempts = 10;
48
49
50
51
52
53
54 protected $processed;
55
56
57
58
59 private $total = 0;
60
61
62
63
64
65 private $sourceHostname;
66
67
68
69
70
71 private $destinationHostname;
72
73
74
75
76
77 private $strings;
78
79
80
81
82
83 public $tmpPrefix;
84
85
86
87
88 public function initialize()
89 {
90 $this->setupMemoryExhaustFile();
91 $this->initializeDbObjects();
92 $this->total = count($this->options->tables);
93 $this->tmpPrefix = $this->options->prefix;
94 $this->strings = new Strings();
95 $this->sourceHostname = $this->getSourceHostname();
96 $this->destinationHostname = $this->getDestinationHostname();
97 }
98
99 public function start()
100 {
101
102 if ($this->options->totalSteps === 0) {
103 $this->prepareResponse(true, false);
104 }
105
106 $this->run();
107
108
109 $this->saveOptions();
110
111 return (object)$this->response;
112 }
113
114
115
116
117
118
119 protected function execute()
120 {
121
122 if ($this->isOverThreshold()) {
123
124 $this->prepareResponse(false, false);
125 $this->saveOptions();
126 return false;
127 }
128
129
130 if ($this->options->currentStep > $this->total || !isset($this->options->tables[$this->options->currentStep])) {
131 $this->prepareResponse(true, false);
132 return false;
133 }
134
135
136 if (in_array($this->options->tables[$this->options->currentStep], $this->options->excludedTables)) {
137 $this->prepareResponse();
138 return true;
139 }
140
141
142 if (!$this->updateTable($this->options->tables[$this->options->currentStep])) {
143
144 $this->prepareResponse(false, false);
145
146
147 return true;
148 }
149
150
151
152 $this->prepareResponse();
153
154
155 return true;
156 }
157
158
159
160
161
162
163 private function updateTable($tableName)
164 {
165 $strings = new Strings();
166 $table = $strings->strReplaceFirst(WPStaging::getTablePrefix(), '', $tableName);
167 $newTableName = $this->tmpPrefix . $table;
168
169
170 $this->setJob($newTableName);
171
172
173 if (!$this->startJob($newTableName, $tableName)) {
174 return true;
175 }
176
177
178 $this->startReplace($newTableName);
179
180
181 return $this->finishStep();
182 }
183
184
185
186
187
188
189
190
191
192
193
194
195 private function getDestinationHostname()
196 {
197
198 if ($this->options->mainJob === Job::UPDATE) {
199
200 if (!empty($this->options->cloneHostname)) {
201 return $this->strings->getUrlWithoutScheme($this->options->cloneHostname);
202 }
203
204 return $this->strings->getUrlWithoutScheme($this->options->destinationHostname);
205 }
206
207
208 if (!empty($this->options->cloneHostname)) {
209 return $this->strings->getUrlWithoutScheme($this->options->cloneHostname);
210 }
211
212
213 if ($this->isSubDir()) {
214 return $this->strings->getUrlWithoutScheme(trailingslashit($this->options->destinationHostname) . $this->getSubDir() . '/' . $this->options->cloneDirectoryName);
215 }
216
217 if ($this->isMultisiteAndPro()) {
218 $multisiteHostname = (new Multisite())->getHomeDomainWithoutScheme();
219
220 $multisitePath = defined('PATH_CURRENT_SITE') ? PATH_CURRENT_SITE : '/';
221
222 return rtrim($multisiteHostname, '/\\') . $multisitePath . $this->options->cloneDirectoryName;
223 }
224
225
226 return $this->strings->getUrlWithoutScheme(trailingslashit($this->options->destinationHostname) . $this->options->cloneDirectoryName);
227 }
228
229
230
231
232
233 private function startReplace($table)
234 {
235 $rows = $this->options->job->start + $this->settings->querySRLimit;
236
237 if ((int)$this->settings->querySRLimit <= 1) {
238 $this->logDebug(sprintf('%s - $this->settings->querySRLimit is too low. Typeof: %s. JSON Encoded Value: %s', __METHOD__, gettype($this->settings->querySRLimit), wp_json_encode($this->settings->querySRLimit)));
239 }
240
241 if ((int)$rows <= 1) {
242 $this->logDebug(sprintf('%s - $rows is too low.', __METHOD__));
243 }
244
245 $this->log(
246 "DB Search & Replace: Table {$table} {$this->options->job->start} to {$rows} records"
247 );
248
249
250 $this->searchReplace($table, []);
251
252 if ($this->isSearchReplaceGeneratorDisabled()) {
253 $this->options->job->start += $this->settings->querySRLimit;
254 }
255 }
256
257
258
259
260
261
262
263
264 protected function getColumns($table)
265 {
266 $primaryKeys = [];
267 $columns = [];
268 $fields = $this->stagingDb->get_results('DESCRIBE ' . $table);
269
270 if (empty($fields)) {
271
272 return false;
273 }
274
275 if (is_array($fields)) {
276 foreach ($fields as $column) {
277 $columns[] = $column->Field;
278 if ($column->Key === 'PRI') {
279 $primaryKeys[] = $column->Field;
280 }
281 }
282 }
283
284 return [$primaryKeys, $columns];
285 }
286
287
288
289
290
291
292
293 private function searchReplace($table, $args)
294 {
295 $table = esc_sql($table);
296
297 $args['search_for'] = $this->generateHostnamePatterns($this->sourceHostname);
298 $args['search_for'][] = ABSPATH;
299
300 $args['replace_with'] = $this->generateHostnamePatterns($this->destinationHostname);
301 $args['replace_with'][] = $this->options->destinationDir;
302
303 $this->debugLog("DB Search & Replace: Search: {$args['search_for'][0]}", Logger::TYPE_INFO);
304 $this->debugLog("DB Search & Replace: Replace: {$args['replace_with'][0]}", Logger::TYPE_INFO);
305
306 $args['replace_guids'] = 'off';
307 $args['dry_run'] = 'off';
308 $args['case_insensitive'] = false;
309 $args['skip_transients'] = 'on';
310
311
312 $args = Hooks::applyFilters(self::FILTER_CLONE_SEARCH_REPLACE_PARAMS, $args);
313
314
315 $primaryKeyAndColumns = $this->getColumns($table);
316
317 if ($primaryKeyAndColumns === false) {
318
319 ++$this->options->job->failedAttempts;
320 return false;
321 }
322
323 list($primaryKeys, $columns) = $primaryKeyAndColumns;
324
325 if ($this->options->job->current !== $table) {
326 $this->logDebug(sprintf('We are using the LIMITS of a table different than the table we are parsing now. Table being parsed: %s. Table that we are using "start" from: %s. Start: %s', $table, $this->options->job->current, $this->options->job->start));
327 }
328
329 $currentRow = 0;
330 $offset = $this->options->job->start;
331 $limit = $this->settings->querySRLimit;
332
333 if ($this->isSearchReplaceGeneratorDisabled()) {
334 $data = $this->stagingDb->get_results("SELECT * FROM $table LIMIT $offset, $limit", ARRAY_A);
335 } else {
336 $this->lastFetchedPrimaryKeyValue = is_object($this->options->job) && property_exists($this->options->job, 'lastProcessedId') ? $this->options->job->lastProcessedId : false;
337 $data = $this->rowsGenerator($table, $offset, $limit, $this->stagingDb);
338 }
339
340
341 $filter = $this->excludedStrings();
342
343 $filter = apply_filters(self::FILTER_CLONE_SEARCH_REPLACE_EXCLUDED_ROWS, $filter);
344
345 $processed = 0;
346
347
348 foreach ($data as $row) {
349 $processed++;
350 $currentRow++;
351 $updateSql = [];
352 $whereSql = [];
353 $doUpdate = false;
354
355 if ($this->lastFetchedPrimaryKeyValue !== false) {
356 $this->lastFetchedPrimaryKeyValue = $row[$this->numericPrimaryKey];
357 }
358
359
360 if (isset($row['option_name']) && in_array($row['option_name'], $filter)) {
361 continue;
362 }
363
364
365 if (isset($row['option_name']) && $args['skip_transients'] === 'on' && strpos($row['option_name'], '_transient') !== false
366 ) {
367 continue;
368 }
369
370
371 if (isset($row['option_value']) && strlen($row['option_value']) >= 5000000) {
372 continue;
373 }
374
375
376 foreach ($columns as $column) {
377 $dataRow = $row[$column];
378
379
380 if (is_null($dataRow)) {
381 continue;
382 }
383
384
385 $size = strlen($dataRow);
386 if ($size >= 5000000) {
387 continue;
388 }
389
390
391 if (in_array($column, $primaryKeys)) {
392 $whereSql[] = $column . ' = "' . WPStaging::make(Escape::class)->mysqlRealEscapeString($dataRow) . '"';
393 continue;
394 }
395
396
397 if ($args['replace_guids'] !== 'on' && $column === 'guid') {
398 continue;
399 }
400
401 $excludes = Hooks::applyFilters(self::FILTER_CLONE_SEARCH_REPLACE_EXCLUDED, []);
402 $searchReplace = new \WPStaging\Framework\Database\SearchReplace($args['search_for'], $args['replace_with'], $args['case_insensitive'], $excludes);
403
404 $siteInfo = WPStaging::make(SiteInfo::class);
405 $searchReplace->setWpBakeryActive($siteInfo->isWpBakeryActive());
406 $dataRow = $searchReplace->replaceExtended($dataRow);
407
408
409 if ($row[$column] !== $dataRow) {
410 $updateSql[] = $column . " = '" . WPStaging::make(Escape::class)->mysqlRealEscapeString($dataRow) . "'";
411 $doUpdate = true;
412 }
413 }
414
415
416 if ($args['dry_run'] === 'on') {
417
418 } elseif ($doUpdate && !empty($whereSql)) {
419
420 $sql = 'UPDATE ' . $table . ' SET ' . implode(', ', $updateSql) . ' WHERE ' . implode(' AND ', array_filter($whereSql));
421 $result = $this->stagingDb->query($sql);
422
423 if ($result === false) {
424 $partialQuery = substr($sql, 0, 100);
425 $this->log(
426 "Error updating row {$currentRow} SQL: {$partialQuery}",
427 Logger::TYPE_ERROR
428 );
429 }
430 }
431 }
432
433 unset($row, $updateSql, $whereSql, $sql, $currentRow);
434
435 if (!$this->isSearchReplaceGeneratorDisabled()) {
436 $this->updateJobStart($processed, $this->stagingDb, $table);
437 }
438
439
440 $this->stagingDb->flush();
441 return true;
442 }
443
444
445
446
447
448 private function setJob($table)
449 {
450 if (!empty($this->options->job->current)) {
451 return;
452 }
453
454
455 if (!is_object($this->options->job)) {
456 $this->options->job = new stdClass();
457 }
458
459 $this->options->job->current = $table;
460 $this->options->job->start = 0;
461 }
462
463
464
465
466
467
468
469 private function startJob($newTableName, $oldTableName)
470 {
471 if ($this->isExcludedTable($newTableName)) {
472 return false;
473 }
474
475
476 $result = $this->productionDb->query("SHOW TABLES LIKE '{$oldTableName}'");
477 if (!$result || $result === 0) {
478 return false;
479 }
480
481 if (!isset($this->options->job->failedAttempts)) {
482 $this->options->job->failedAttempts = 0;
483 }
484
485 if ($this->options->job->start !== 0) {
486
487 return !($this->options->job->failedAttempts > $this->maxFailedAttempts);
488 }
489
490 $this->options->job->total = (int)$this->productionDb->get_var("SELECT COUNT(1) FROM {$oldTableName}");
491 $this->options->job->failedAttempts = 0;
492
493 if ($this->options->job->total === 0) {
494 $this->finishStep();
495 return false;
496 }
497
498 return true;
499 }
500
501
502
503
504
505
506 private function isExcludedTable($table)
507 {
508 $tables = $this->excludedTableService->getExcludedTablesForSearchReplace($this->isNetworkClone());
509
510 $excludedAllTables = [];
511 foreach ($tables as $key => $value) {
512 $excludedAllTables[] = $this->options->prefix . ltrim($value, '_');
513 }
514
515 if (in_array($table, $excludedAllTables)) {
516 $this->log("DB Search & Replace: Table {$table} excluded by WP STAGING", Logger::TYPE_INFO);
517 return true;
518 }
519
520 return false;
521 }
522
523
524
525
526 protected function finishStep()
527 {
528
529 if (!$this->noResultRows && ($this->options->job->total > $this->options->job->start)) {
530 return false;
531 }
532
533
534 $this->options->clonedTables[] = $this->options->tables[$this->options->currentStep];
535
536
537 $this->options->job = new stdClass();
538
539 return true;
540 }
541
542
543
544
545
546
547
548
549
550
551
552
553 protected function updateJobStart($processed, wpdb $db, $table)
554 {
555 $this->processed = absint($processed);
556
557
558
559 if ($this->executeNumericPrimaryKeyQuery && $this->lastFetchedPrimaryKeyValue !== false) {
560 $this->options->job->lastProcessedId = $this->lastFetchedPrimaryKeyValue;
561 $this->options->job->start += $this->processed;
562 return;
563 }
564
565
566 $minimumProcessed = 1;
567
568
569
570
571
572
573 if ($this->processed === 0) {
574 $this->logDebug('SEARCH_REPLACE: Processed is zero');
575
576 $totalRowsInTable = $db->get_var("SELECT COUNT(*) FROM $table");
577
578 if (is_numeric($totalRowsInTable)) {
579 $this->logDebug("SEARCH_REPLACE: Rows count is numeric: $totalRowsInTable");
580
581 $minimumProcessed = min(max((int)$totalRowsInTable / 100, 1), $this->settings->querySRLimit);
582 } else {
583 $this->logDebug(sprintf("SEARCH_REPLACE: Rows count is not numeric. Type: %s. Json encoded value: %s", gettype($totalRowsInTable), wp_json_encode($totalRowsInTable)));
584
585 $minimumProcessed = $this->settings->querySRLimit;
586 }
587
588 $this->logDebug("SEARCH_REPLACE: Minimum processed is: $minimumProcessed");
589 }
590
591 $this->options->job->start += max($processed, $minimumProcessed);
592 }
593
594
595
596
597
598
599
600 public function getProcessed()
601 {
602 return $this->processed;
603 }
604
605 protected function logDebug($message)
606 {
607 \WPStaging\functions\debug_log($message, 'debug');
608 }
609
610
611
612
613 protected function isSearchReplaceGeneratorDisabled(): bool
614 {
615 if (!defined('WPSTG_DISABLE_SEARCH_REPLACE_GENERATOR')) {
616 return false;
617 }
618
619 return constant('WPSTG_DISABLE_SEARCH_REPLACE_GENERATOR');
620 }
621 }
622