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