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