PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backup, Restore, Migration & Clone / 4.9.2
WP STAGING – WordPress Backup, Restore, Migration & Clone v4.9.2
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 / Database.php
wp-staging / Backend / Modules / Jobs Last commit date
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
Database.php
524 lines
1 <?php
2
3 namespace WPStaging\Backend\Modules\Jobs;
4
5 use stdClass;
6 use WPStaging\Backend\Modules\Jobs\Exceptions\FatalException;
7 use WPStaging\Core\Utils\Logger;
8 use WPStaging\Core\WPStaging;
9 use WPStaging\Framework\CloningProcess\CloningDto;
10 use WPStaging\Framework\CloningProcess\Database\DatabaseCloningService;
11 use WPStaging\Framework\Adapter\Database as DatabaseAdapter;
12 use WPStaging\Framework\Database\TableService;
13 use WPStaging\Framework\Traits\TablePrefixValidator;
14 use WPStaging\Framework\Facades\Hooks;
15 use WPStaging\Framework\Filesystem\Filesystem;
16 use WPStaging\Staging\Service\Database\RowsExporter;
17
18 /**
19 * Class Database
20 * @package WPStaging\Backend\Modules\Jobs
21 */
22 class Database extends CloningProcess
23 {
24 use TotalStepsAreNumberOfTables;
25 use TablePrefixValidator;
26
27 /**
28 * @var DatabaseCloningService
29 */
30 private $databaseCloningService;
31
32 /**
33 * @var int
34 */
35 private $total = 0;
36
37 /**
38 * Initialize
39 * @throws \Exception
40 */
41 public function initialize()
42 {
43 $this->setupMemoryExhaustFile();
44 $this->initializeDbObjects();
45 $this->abortIfDirectoryNotEmpty();
46 $this->abortIfDirectoryNotCreated();
47 $this->abortIfPrefixContainsInvalidCharacter();
48 if (!$this->isExternalDatabase()) {
49 $this->abortIfStagingPrefixEqualsProdPrefix();
50 } else {
51 $this->abortIfExternalButNotPro();
52 }
53
54 $this->generateDto();
55 $this->addMissingTables();
56 $this->total = count($this->options->tables);
57 // if mainJob is 'Reset', add one extra pre step for deleting all tables
58 if ($this->options->mainJob === Job::RESET) {
59 $this->total++;
60 }
61 }
62
63 /**
64 *
65 */
66 protected function generateDto()
67 {
68 $this->databaseCloningService = new DatabaseCloningService(
69 new CloningDto(
70 $this,
71 $this->stagingDb,
72 $this->productionDb,
73 $this->isExternalDatabase(),
74 $this->isMultisiteAndPro(),
75 $this->isExternalDatabase() ? $this->options->databaseServer : null,
76 $this->isExternalDatabase() ? $this->options->databaseUser : null,
77 $this->isExternalDatabase() ? $this->options->databasePassword : null,
78 $this->isExternalDatabase() ? $this->options->databaseDatabase : null,
79 $this->isExternalDatabase() ? $this->options->databaseSsl : false
80 )
81 );
82 }
83
84 /**
85 * Execute the Current Step
86 * Returns false when over threshold limits are hit or when the job is done, true otherwise
87 * @return bool
88 * @throws \Exception
89 */
90 protected function execute()
91 {
92 // Over limits threshold
93 if ($this->isOverThreshold()) {
94 // Prepare response and save current progress
95 $this->prepareResponse(false, false);
96 $this->saveOptions();
97 return false;
98 }
99
100 // No more steps, finished
101 if ($this->options->currentStep > $this->total || !$this->isRunning()) {
102 $this->prepareResponse(true, false);
103 return false;
104 }
105
106 if (!$this->deleteAllTables()) {
107 // Prepare Response
108 $this->prepareResponse(false, false);
109
110 // Not finished
111 return true;
112 }
113
114 // decrement the tableIndex if mainJob is 'resetting'
115 $tableIndex = $this->options->currentStep;
116 if ($this->options->mainJob === Job::RESET) {
117 $tableIndex--;
118 }
119
120 // Copy table
121 if (isset($this->options->tables[$tableIndex]) && !$this->copyTable($this->options->tables[$tableIndex])) {
122 // Prepare Response
123 $this->prepareResponse(false, false);
124
125 // Not finished
126 return true;
127 }
128
129 $this->prepareResponse();
130
131 // Not finished
132 return true;
133 }
134
135 /**
136 * Delete all tables in staging site if the mainJob is 'resetting'
137 *
138 * @return bool
139 * @throws \Exception
140 */
141 private function deleteAllTables()
142 {
143 if ($this->options->mainJob !== Job::RESET) {
144 return true;
145 }
146
147 if ($this->options->currentStep !== 0) {
148 return true;
149 }
150
151 if (!isset($this->options->databaseResettingStatus)) {
152 $this->options->databaseResettingStatus = 'pending';
153 $this->saveOptions();
154 }
155
156 if ($this->options->databaseResettingStatus === 'finished') {
157 return true;
158 }
159
160 if ($this->options->databaseResettingStatus === 'pending') {
161 $this->log('#################### Start Reset Job ####################');
162 $this->log('DB: Removing all staging database tables.');
163 $this->options->databaseResettingStatus = 'processing';
164 $this->saveOptions();
165 }
166
167 // TODO: inject using DI
168 $tableService = new TableService(new DatabaseAdapter($this->stagingDb));
169 $tableService->setShouldStop([$this, 'isOverThreshold']);
170 if (!$tableService->deleteTablesStartWith($this->getStagingPrefix())) {
171 return false;
172 }
173
174 $this->options->databaseResettingStatus = 'finished';
175 $this->saveOptions();
176
177 $this->prepareResponse();
178 return true;
179 }
180
181 /**
182 * Check if table already exists
183 * @param string $name
184 * @return bool
185 */
186 private function isTableExist($name)
187 {
188 $old = $this->stagingDb->get_var($this->stagingDb->prepare("SHOW TABLES LIKE %s", $name));
189
190 return (
191 $old === $name &&
192 (
193 !isset($this->options->job->current, $this->options->job->start) || $this->options->job->start === 0
194 )
195 );
196 }
197
198 /**
199 * Check if table already exists and the main job is not updating
200 * @param string $name
201 * @return bool
202 */
203 private function shouldAbortIfTableExist($name)
204 {
205 return isset($this->options->mainJob) && $this->options->mainJob !== Job::UPDATE && $this->isTableExist($name);
206 }
207
208 /**
209 * Finish the step
210 */
211 private function finishStep()
212 {
213 // This job is not finished yet
214 if ($this->options->job->total > $this->options->job->start) {
215 return false;
216 }
217
218 $this->finishDataCopying();
219
220 return true;
221 }
222
223 /**
224 * @return void
225 */
226 private function finishDataCopying()
227 {
228 // Add it to cloned tables listing
229 $this->options->clonedTables[] = isset($this->options->tables[$this->options->currentStep]) ? $this->options->tables[$this->options->currentStep] : false;
230
231 // Reset job
232 $this->options->job = new stdClass();
233 }
234
235 /**
236 * Check if external database is used and if It's not pro version
237 * @return bool
238 */
239 private function abortIfExternalButNotPro()
240 {
241 if (defined('WPSTGPRO_VERSION')) {
242 return false;
243 }
244
245 $this->returnException(__("This staging site is located in another database and needs to be edited with <a href='https://wp-staging.com' target='_blank'>WP STAGING Pro</a>", "wp-staging"));
246
247 return true;
248 }
249
250 /**
251 * Set the job
252 * @param string $table
253 */
254 private function setJob($table)
255 {
256 if (isset($this->options->job->current)) {
257 return;
258 }
259
260 // Create job object if not exists
261 if (!is_object($this->options->job)) {
262 $this->options->job = new stdClass();
263 }
264
265 $this->options->job->current = $table;
266 $this->options->job->start = 0;
267 }
268
269 /**
270 * @param $srcTableName string|object
271 * @return bool
272 * @throws \Exception
273 */
274 private function copyTable($srcTableName)
275 {
276 $srcTableName = is_object($srcTableName) ? $srcTableName->name : $srcTableName;
277
278 $tableWithoutPrefix = $this->databaseCloningService->removeDBPrefix($srcTableName);
279 $destTableName = $this->getStagingPrefix() . $tableWithoutPrefix;
280
281 if ($this->isMultisiteAndPro()) {
282 // Build full name of table 'users' from main site e.g. wp_users
283 if ($tableWithoutPrefix === 'users') {
284 $srcTableName = $this->productionDb->base_prefix . 'users';
285 }
286
287 // Build full name of table 'usermeta' from main site e.g. wp_usermeta
288 if ($tableWithoutPrefix === 'usermeta') {
289 $srcTableName = $this->productionDb->base_prefix . 'usermeta';
290 }
291 }
292
293 if (!$this->isCopyProcessStarted() && $this->shouldAbortIfTableExist($destTableName)) {
294 $this->returnException(sprintf(__("Can not proceed. Tables beginning with the prefix '%s' already exist in the database i.e. %s. Choose another table prefix and try again.", "wp-staging"), $this->getStagingPrefix(), $destTableName));
295 return true;
296 }
297
298 $this->setJob($destTableName);
299
300 if (!$this->startJob($destTableName, $srcTableName)) {
301 return true;
302 }
303
304 $tablesToExcludeData = Hooks::applyFilters(RowsExporter::FILTER_EXCLUDE_TABLES_DATA, RowsExporter::TABLES_EXCLUDED_FROM_DATA_COPYING);
305 if (in_array($tableWithoutPrefix, $tablesToExcludeData)) {
306 $this->log("Skipping data copy for table {$srcTableName}", Logger::TYPE_INFO);
307 $this->finishDataCopying();
308 return true;
309 }
310
311 $this->copyData($destTableName, $srcTableName);
312
313 return $this->finishStep();
314 }
315
316 /**
317 * Copy data from old table to new table
318 * @param string $destTableName
319 * @param string $srcTableName
320 */
321 private function copyData($destTableName, $srcTableName)
322 {
323 $this->databaseCloningService->copyData($srcTableName, $destTableName, $this->options->job->start, $this->settings->queryLimit);
324 // Set new offset
325 $this->options->job->start += $this->settings->queryLimit;
326 }
327
328 /**
329 * Is table excluded from database copying processing?
330 * @param string $table
331 * @return bool
332 */
333 private function isExcludedTable(string $table)
334 {
335
336 if (
337 in_array(
338 $table,
339 array_map(
340 function ($tableName) {
341 return $this->options->prefix . $tableName;
342 },
343 $this->excludedTableService->getExcludedTables($this->isNetworkClone())
344 )
345 )
346 ) {
347 return true;
348 }
349
350 return false;
351 }
352
353 /**
354 * Start Job and create tables
355 * @param string $destinationTable
356 * @param string $sourceTable
357 * @return bool
358 * @throws \Exception
359 */
360 private function startJob($destinationTable, $sourceTable)
361 {
362 if ($this->isExcludedTable($destinationTable)) {
363 return false;
364 }
365
366 if ($this->options->job->start !== 0) {
367 return true;
368 }
369
370 if ($this->databaseCloningService->isMissingTable($sourceTable)) {
371 return true;
372 }
373
374 try {
375 $this->options->job->total = 0;
376 $this->options->job->total = $this->databaseCloningService->createTable($sourceTable, $destinationTable);
377 } catch (FatalException $e) {
378 $this->log($e->getMessage(), Logger::TYPE_WARNING);
379 $this->log(__('Skipping cloning table: ' . $sourceTable, 'wp-staging'), Logger::TYPE_WARNING);
380 $this->finishStep();
381 return false;
382 }
383
384 if ($this->options->job->total === 0) {
385 $this->finishStep();
386 return false;
387 }
388
389 $this->options->job->copyProcessStarted = true;
390 $this->saveOptions();
391 return true;
392 }
393
394 /**
395 * Add wp_users and wp_usermeta to the tables if they do not exist
396 * because they are not available in MU installation, but we need them on the staging site
397 *
398 * return void
399 * @throws \Exception
400 */
401 private function addMissingTables()
402 {
403 $dbPrefix = WPStaging::getTablePrefix();
404 // Early bail: if updating
405 if (isset($this->options->mainJob) && $this->options->mainJob === Job::UPDATE) {
406 return;
407 }
408
409 if (!in_array($dbPrefix . 'users', $this->options->tables)) {
410 $this->options->tables[] = $dbPrefix . 'users';
411 $this->saveOptions();
412 }
413
414 if (!in_array($dbPrefix . 'usermeta', $this->options->tables)) {
415 $this->options->tables[] = $dbPrefix . 'usermeta';
416 $this->saveOptions();
417 }
418 }
419
420 /**
421 * @return bool
422 */
423 private function abortIfStagingPrefixEqualsProdPrefix()
424 {
425 $dbPrefix = WPStaging::getTablePrefix();
426 if ($dbPrefix === $this->getStagingPrefix()) {
427 $error = 'Fatal error 7: The destination database table prefix ' . $this->getStagingPrefix() . ' is identical to the table prefix of the production site. Go to Sites > Actions > Edit Data and correct the table prefix or contact us.';
428 $this->returnException($error);
429 return true;
430 }
431
432 return false;
433 }
434
435 /**
436 * Get new prefix for the staging site
437 * @return string
438 */
439 protected function getStagingPrefix()
440 {
441 if ($this->isExternalDatabase()) {
442 $this->options->prefix = !empty($this->options->databasePrefix) ? $this->options->databasePrefix : $this->productionDb->prefix;
443 }
444
445 if (strncasecmp(PHP_OS, 'WIN', 3) === 0) {
446 return strtolower($this->options->prefix);
447 }
448
449 return $this->options->prefix;
450 }
451
452 /**
453 * Return fatal error and stops here if sub folder already exists
454 * and mainJob is not updating and resetting the clone
455 * @return bool
456 */
457 private function abortIfDirectoryNotEmpty()
458 {
459 $path = trailingslashit($this->options->cloneDir);
460 if (isset($this->options->mainJob) && $this->options->mainJob !== Job::RESET && $this->options->mainJob !== Job::UPDATE && is_dir($path) && !wpstg_is_empty_dir($path)) {
461 $this->returnException(" Can not continue for security purposes. Directory {$path} is not empty! Use FTP or a file manager plugin and make sure it does not contain any files. ");
462 return true;
463 }
464
465 return false;
466 }
467
468 /**
469 * Return fatal error, if unable to create staging site directory
470 * @return bool
471 */
472 private function abortIfDirectoryNotCreated()
473 {
474 // Early bail if not a new staging site
475 if (isset($this->options->mainJob) && ($this->isUpdateOrResetJob())) {
476 return false;
477 }
478
479 // Early bail if directory already exists
480 $path = trailingslashit($this->options->cloneDir);
481 if (is_dir($path)) {
482 return false;
483 }
484
485 $fs = new Filesystem();
486 if ($fs->mkdir($path)) {
487 return false;
488 }
489
490 $this->returnException(" Unable to create the staging site directory $path " . $fs->getLogs()[0]);
491 return true;
492 }
493
494 /**
495 * Stop cloning if database prefix contains hyphen
496 * @return bool
497 */
498 private function abortIfPrefixContainsInvalidCharacter()
499 {
500 // make sure prefix doesn't contain any invalid character
501 // same condition as in WordPress wpdb::set_prefix() method
502 if (preg_match('|[^a-z0-9_]|i', $this->options->databasePrefix)) {
503 $this->returnException(__("Table prefix contains invalid character(s). Use different prefix with valid characters.", 'wp-staging'));
504 return true;
505 }
506
507 if ($this->isWpStagingReservedPrefix($this->options->databasePrefix)) {
508 $this->returnException($this->getReservedPrefixErrorMessage($this->options->databasePrefix));
509 return true;
510 }
511
512 return false;
513 }
514
515 /**
516 * Check if the copy process started or not.
517 * @return bool
518 */
519 private function isCopyProcessStarted()
520 {
521 return isset($this->options->job->copyProcessStarted) && $this->options->job->copyProcessStarted === true;
522 }
523 }
524