Cleaners
3 months ago
Exceptions
5 years ago
Cancel.php
7 months ago
CancelUpdate.php
7 months ago
Cloning.php
2 weeks ago
CloningProcess.php
1 year ago
Data.php
7 months ago
Database.php
3 months ago
Delete.php
4 months ago
Directories.php
4 months ago
Files.php
1 week ago
Finish.php
2 months ago
Job.php
2 weeks ago
JobExecutable.php
7 months ago
Logs.php
3 years ago
PreserveDataFirstStep.php
1 month ago
PreserveDataSecondStep.php
1 month ago
ProcessLock.php
1 year ago
Scan.php
4 months ago
SearchReplace.php
5 months ago
TotalStepsAreNumberOfTables.php
5 years ago
Updating.php
2 weeks 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 | * Class SearchReplace |
| 19 | * |
| 20 | * Used for CLONING |
| 21 | * @see \WPStaging\Backend\Pro\Modules\Jobs\SearchReplace Used for PUSHING |
| 22 | * |
| 23 | * @todo Unify those |
| 24 | * |
| 25 | * @package WPStaging\Backend\Modules\Jobs |
| 26 | */ |
| 27 | class SearchReplace extends CloningProcess |
| 28 | { |
| 29 | use TotalStepsAreNumberOfTables; |
| 30 | use DbRowsGeneratorTrait; |
| 31 | use DatabaseSearchReplaceTrait; |
| 32 | |
| 33 | /** @var string */ |
| 34 | const FILTER_CLONE_SEARCH_REPLACE_EXCLUDED_ROWS = 'wpstg_clone_searchreplace_excl_rows'; |
| 35 | |
| 36 | /** @var string */ |
| 37 | const FILTER_CLONE_SEARCH_REPLACE_EXCLUDED = 'wpstg_clone_searchreplace_excl'; |
| 38 | |
| 39 | /** @var string */ |
| 40 | const FILTER_CLONE_SEARCH_REPLACE_PARAMS = 'wpstg_clone_searchreplace_params'; |
| 41 | |
| 42 | /** |
| 43 | * The maximum number of failed attempts after which the Job should just move on. |
| 44 | * |
| 45 | * @var int |
| 46 | */ |
| 47 | protected $maxFailedAttempts = 10; |
| 48 | |
| 49 | /** |
| 50 | * The number of processed items, or `null` if the job did not run yet. |
| 51 | * |
| 52 | * @var int|null |
| 53 | */ |
| 54 | protected $processed; |
| 55 | |
| 56 | /** |
| 57 | * @var int |
| 58 | */ |
| 59 | private $total = 0; |
| 60 | |
| 61 | /** |
| 62 | * |
| 63 | * @var string |
| 64 | */ |
| 65 | private $sourceHostname; |
| 66 | |
| 67 | /** |
| 68 | * |
| 69 | * @var string |
| 70 | */ |
| 71 | private $destinationHostname; |
| 72 | |
| 73 | /** |
| 74 | * |
| 75 | * @var Strings |
| 76 | */ |
| 77 | private $strings; |
| 78 | |
| 79 | /** |
| 80 | * The prefix of the new database tables which are used for the live site after updating tables |
| 81 | * @var string |
| 82 | */ |
| 83 | public $tmpPrefix; |
| 84 | |
| 85 | /** |
| 86 | * Initialize |
| 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 | // Skip job. Nothing to do |
| 102 | if ($this->options->totalSteps === 0) { |
| 103 | $this->prepareResponse(true, false); |
| 104 | } |
| 105 | |
| 106 | $this->run(); |
| 107 | |
| 108 | // Save option, progress |
| 109 | $this->saveOptions(); |
| 110 | |
| 111 | return (object)$this->response; |
| 112 | } |
| 113 | |
| 114 | /** |
| 115 | * Execute the Current Step |
| 116 | * Returns false when over threshold limits are hit or when the job is done, true otherwise |
| 117 | * @return bool |
| 118 | */ |
| 119 | protected function execute() |
| 120 | { |
| 121 | // Over limits threshold |
| 122 | if ($this->isOverThreshold()) { |
| 123 | // Prepare response and save current progress |
| 124 | $this->prepareResponse(false, false); |
| 125 | $this->saveOptions(); |
| 126 | return false; |
| 127 | } |
| 128 | |
| 129 | // No more steps, finished |
| 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 | // Table is excluded |
| 136 | if (in_array($this->options->tables[$this->options->currentStep], $this->options->excludedTables)) { |
| 137 | $this->prepareResponse(); |
| 138 | return true; |
| 139 | } |
| 140 | |
| 141 | // Search & Replace |
| 142 | if (!$this->updateTable($this->options->tables[$this->options->currentStep])) { |
| 143 | // Prepare Response |
| 144 | $this->prepareResponse(false, false); |
| 145 | |
| 146 | // Not finished |
| 147 | return true; |
| 148 | } |
| 149 | |
| 150 | |
| 151 | // Prepare Response |
| 152 | $this->prepareResponse(); |
| 153 | |
| 154 | // Not finished |
| 155 | return true; |
| 156 | } |
| 157 | |
| 158 | /** |
| 159 | * Copy Tables |
| 160 | * @param string $tableName |
| 161 | * @return bool |
| 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 | // Save current job |
| 170 | $this->setJob($newTableName); |
| 171 | |
| 172 | // Beginning of the job |
| 173 | if (!$this->startJob($newTableName, $tableName)) { |
| 174 | return true; |
| 175 | } |
| 176 | |
| 177 | // Copy data |
| 178 | $this->startReplace($newTableName); |
| 179 | |
| 180 | // Finish the step |
| 181 | return $this->finishStep(); |
| 182 | } |
| 183 | |
| 184 | /** |
| 185 | * Get destination hostname without scheme e.g example.com/staging or staging.example.com |
| 186 | * |
| 187 | * Conditions: |
| 188 | * - Main job is 'update' |
| 189 | * - WP installed in sub dir |
| 190 | * - Target hostname in advanced settings defined (Pro version only) |
| 191 | * |
| 192 | * @return string |
| 193 | * @todo Complex conditions. Might need refactor |
| 194 | */ |
| 195 | private function getDestinationHostname() |
| 196 | { |
| 197 | // Update process: Neither 'push' nor 'clone' |
| 198 | if ($this->options->mainJob === Job::UPDATE) { |
| 199 | // Defined and created in advanced settings with pro version |
| 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 | // Clone process: Defined and created in advanced settings with pro version |
| 208 | if (!empty($this->options->cloneHostname)) { |
| 209 | return $this->strings->getUrlWithoutScheme($this->options->cloneHostname); |
| 210 | } |
| 211 | |
| 212 | // Clone process: WP installed in sub directory under root |
| 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 | // Relative path to root of main multisite without leading or trailing slash e.g.: wordpress |
| 220 | $multisitePath = defined('PATH_CURRENT_SITE') ? PATH_CURRENT_SITE : '/'; |
| 221 | |
| 222 | return rtrim($multisiteHostname, '/\\') . $multisitePath . $this->options->cloneDirectoryName; |
| 223 | } |
| 224 | |
| 225 | // Clone process: Default |
| 226 | return $this->strings->getUrlWithoutScheme(trailingslashit($this->options->destinationHostname) . $this->options->cloneDirectoryName); |
| 227 | } |
| 228 | |
| 229 | /** |
| 230 | * Start search replace job |
| 231 | * @param string $table |
| 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 | // Search & Replace |
| 250 | $this->searchReplace($table, []); |
| 251 | |
| 252 | if ($this->isSearchReplaceGeneratorDisabled()) { |
| 253 | $this->options->job->start += $this->settings->querySRLimit; |
| 254 | } |
| 255 | } |
| 256 | |
| 257 | /** |
| 258 | * Gets the columns in a table. |
| 259 | * @access public |
| 260 | * @param string $table The table to check. |
| 261 | * @return array|false Either the primary key and columns structures, or `false` to indicate the query |
| 262 | * failed or the table is not describe-able. |
| 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 | // Either there was an error or the table has no columns. |
| 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 | * @param string $table The table to run the replacement on. |
| 290 | * @param array $args An associative array containing arguments for this run. |
| 291 | * @return bool Whether the search-replace operation was successful or not. |
| 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 | // Allow filtering of search & replace parameters |
| 312 | $args = Hooks::applyFilters(self::FILTER_CLONE_SEARCH_REPLACE_PARAMS, $args); |
| 313 | |
| 314 | // Get columns and primary keys |
| 315 | $primaryKeyAndColumns = $this->getColumns($table); |
| 316 | |
| 317 | if ($primaryKeyAndColumns === false) { |
| 318 | // Stop here: for some reason the table cannot be described or there was an error. |
| 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 | // Filter certain rows (of other plugins) |
| 341 | $filter = $this->excludedStrings(); |
| 342 | |
| 343 | $filter = apply_filters(self::FILTER_CLONE_SEARCH_REPLACE_EXCLUDED_ROWS, $filter); |
| 344 | |
| 345 | $processed = 0; |
| 346 | |
| 347 | // Go through the table rows |
| 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 | // Skip rows |
| 360 | if (isset($row['option_name']) && in_array($row['option_name'], $filter)) { |
| 361 | continue; |
| 362 | } |
| 363 | |
| 364 | // Skip transients (There can be thousands of them. Save memory and increase performance) |
| 365 | if (isset($row['option_name']) && $args['skip_transients'] === 'on' && strpos($row['option_name'], '_transient') !== false |
| 366 | ) { |
| 367 | continue; |
| 368 | } |
| 369 | |
| 370 | // Skip rows with more than 5MB to save memory. These rows contain log data or something similiar but never site relevant data |
| 371 | if (isset($row['option_value']) && strlen($row['option_value']) >= 5000000) { |
| 372 | continue; |
| 373 | } |
| 374 | |
| 375 | // Go through the columns |
| 376 | foreach ($columns as $column) { |
| 377 | $dataRow = $row[$column]; |
| 378 | |
| 379 | // Don't use empty() here, because it will also skip valid '0' values |
| 380 | if (is_null($dataRow)) { |
| 381 | continue; |
| 382 | } |
| 383 | |
| 384 | // Skip column larger than 5MB |
| 385 | $size = strlen($dataRow); |
| 386 | if ($size >= 5000000) { |
| 387 | continue; |
| 388 | } |
| 389 | |
| 390 | // Skip primary key column |
| 391 | if (in_array($column, $primaryKeys)) { |
| 392 | $whereSql[] = $column . ' = "' . WPStaging::make(Escape::class)->mysqlRealEscapeString($dataRow) . '"'; |
| 393 | continue; |
| 394 | } |
| 395 | |
| 396 | // Skip GUIDs by default. |
| 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 | /** @var SiteInfo */ |
| 404 | $siteInfo = WPStaging::make(SiteInfo::class); |
| 405 | $searchReplace->setWpBakeryActive($siteInfo->isWpBakeryActive()); |
| 406 | $dataRow = $searchReplace->replaceExtended($dataRow); |
| 407 | |
| 408 | // Something was changed |
| 409 | if ($row[$column] !== $dataRow) { |
| 410 | $updateSql[] = $column . " = '" . WPStaging::make(Escape::class)->mysqlRealEscapeString($dataRow) . "'"; |
| 411 | $doUpdate = true; |
| 412 | } |
| 413 | } |
| 414 | |
| 415 | // Determine what to do with updates. |
| 416 | if ($args['dry_run'] === 'on') { |
| 417 | // Don't do anything if a dry run |
| 418 | } elseif ($doUpdate && !empty($whereSql)) { |
| 419 | // If there are changes to make, run the query. |
| 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 | } // end row loop |
| 432 | |
| 433 | unset($row, $updateSql, $whereSql, $sql, $currentRow); |
| 434 | |
| 435 | if (!$this->isSearchReplaceGeneratorDisabled()) { |
| 436 | $this->updateJobStart($processed, $this->stagingDb, $table); |
| 437 | } |
| 438 | |
| 439 | // DB Flush |
| 440 | $this->stagingDb->flush(); |
| 441 | return true; |
| 442 | } |
| 443 | |
| 444 | /** |
| 445 | * Set the job |
| 446 | * @param string $table |
| 447 | */ |
| 448 | private function setJob($table) |
| 449 | { |
| 450 | if (!empty($this->options->job->current)) { |
| 451 | return; |
| 452 | } |
| 453 | |
| 454 | // Create job object if not exists |
| 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 | * Start Job |
| 465 | * @param string $newTableName |
| 466 | * @param string $oldTableName |
| 467 | * @return bool |
| 468 | */ |
| 469 | private function startJob($newTableName, $oldTableName) |
| 470 | { |
| 471 | if ($this->isExcludedTable($newTableName)) { |
| 472 | return false; |
| 473 | } |
| 474 | |
| 475 | // Table does not exist |
| 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 | // The job was attempted too many times and should be skipped now. |
| 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 | * Is table excluded from search replace processing? |
| 503 | * @param string $table |
| 504 | * @return boolean |
| 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 | * Finish the step |
| 525 | */ |
| 526 | protected function finishStep() |
| 527 | { |
| 528 | // This job is not finished yet |
| 529 | if (!$this->noResultRows && ($this->options->job->total > $this->options->job->start)) { |
| 530 | return false; |
| 531 | } |
| 532 | |
| 533 | // Add it to cloned tables listing |
| 534 | $this->options->clonedTables[] = $this->options->tables[$this->options->currentStep]; |
| 535 | |
| 536 | // Reset job |
| 537 | $this->options->job = new stdClass(); |
| 538 | |
| 539 | return true; |
| 540 | } |
| 541 | |
| 542 | /** |
| 543 | * Updates the (next) job start to reflect the number of actually processed rows. |
| 544 | * |
| 545 | * If nothing was processed, then the job start will be ticked by 1. |
| 546 | * |
| 547 | * @param int $processed The number of actually processed rows in this run. |
| 548 | * @param wpdb $db The wpdb instance being used to process. |
| 549 | * @param string $table The table being processed. |
| 550 | * |
| 551 | * @return void The method does not return any value. |
| 552 | */ |
| 553 | protected function updateJobStart($processed, wpdb $db, $table) |
| 554 | { |
| 555 | $this->processed = absint($processed); |
| 556 | |
| 557 | // If it is a numeric primary key table execution, |
| 558 | // Save the last processed primary key value for the next request |
| 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 | // We make sure to increment the offset at least in 1 to avoid infinite loops. |
| 566 | $minimumProcessed = 1; |
| 567 | |
| 568 | /* |
| 569 | * There are some scenarios where we couldn't process any rows in this request. |
| 570 | * The exact causes of this is still under investigation, but to mitigate this |
| 571 | * effect, we will smartly set the offset for the next job based on some context. |
| 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 | // Skip 1% of the current table on each iteration, with a minimum of 1 and a maximum of the query limit. |
| 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 | // Unexpected result from query. Set the offset to the limit. |
| 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 | * Returns the number of rows processed by the job. |
| 596 | * |
| 597 | * @return int|null Either the number of rows processed by the Job, or `null` if the Job did |
| 598 | * not run yet. |
| 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 | * @return bool |
| 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 |