WP Staging – DB & File Duplicator & Migration - Version 2.6.4

Version Description

  • Fix: Broken image folder with duplicate leading slash after cloning
Download this release

Release Info

Developer ReneHermi
Plugin Icon 128x128 WP Staging – DB & File Duplicator & Migration
Version 2.6.4
Comparing to
See all releases

Code changes from version 2.6.3 to 2.6.4

apps/Backend/Administrator.php CHANGED
@@ -283,7 +283,7 @@ class Administrator extends InjectionAware {
283
 
284
  nocache_headers();
285
  header( "Content-Type: text/plain" );
286
- header( "Content-Disposition: attachment; filename='wpstg-system-info.txt'" );
287
  echo wp_strip_all_tags( new SystemInfo( $this->di ) );
288
  }
289
 
283
 
284
  nocache_headers();
285
  header( "Content-Type: text/plain" );
286
+ header( 'Content-Disposition: attachment; filename="wpstg-system-info.txt"' );
287
  echo wp_strip_all_tags( new SystemInfo( $this->di ) );
288
  }
289
 
apps/Backend/Modules/Jobs/Data.php CHANGED
@@ -995,6 +995,9 @@ define( 'DB_COLLATE', '" . DB_COLLATE . "' );\r\n";
995
  // Get UPLOADS from wp-config.php if there is already one
996
  preg_match( "/define\s*\(\s*['\"]UPLOADS['\"]\s*,\s*(.*)\s*\);/", $content, $matches );
997
  $uploadFolder = wpstg_get_rel_upload_dir();
 
 
 
998
  if( !empty( $matches[0] ) ) {
999
  $pattern = "/define\s*\(\s*'UPLOADS'\s*,\s*(.*)\s*\);/";
1000
 
995
  // Get UPLOADS from wp-config.php if there is already one
996
  preg_match( "/define\s*\(\s*['\"]UPLOADS['\"]\s*,\s*(.*)\s*\);/", $content, $matches );
997
  $uploadFolder = wpstg_get_rel_upload_dir();
998
+ $uploadFolder = ltrim($uploadFolder, '/');
999
+ $uploadFolder = rtrim($uploadFolder, '/');
1000
+
1001
  if( !empty( $matches[0] ) ) {
1002
  $pattern = "/define\s*\(\s*'UPLOADS'\s*,\s*(.*)\s*\);/";
1003
 
apps/Backend/Modules/Jobs/Multisite/DataExternal.php CHANGED
@@ -1,1430 +1,1430 @@
1
- <?php
2
-
3
- namespace WPStaging\Backend\Modules\Jobs\Multisite;
4
-
5
- // No Direct Access
6
- if( !defined( "WPINC" ) ) {
7
- die;
8
- }
9
-
10
- use WPStaging\Utils\Logger;
11
- use WPStaging\WPStaging;
12
- use WPStaging\Utils\Helper;
13
- use WPStaging\Utils\Multisite;
14
- use WPStaging\Utils\Strings;
15
- use WPStaging\Backend\Modules\Jobs\JobExecutable;
16
-
17
- /**
18
- * Class Data
19
- * @package WPStaging\Backend\Modules\Jobs
20
- */
21
- class DataExternal extends JobExecutable {
22
-
23
- /**
24
- * @var \wpdb
25
- */
26
- private $db;
27
-
28
- /**
29
- * @var string
30
- */
31
- private $prefix;
32
-
33
- /**
34
- * Tables e.g wpstg3_options
35
- * @var array
36
- */
37
- private $tables;
38
-
39
- /**
40
- * Initialize
41
- */
42
- public function initialize() {
43
- $this->db = $this->getStagingDB();
44
- $this->productionDb = WPStaging::getInstance()->get( "wpdb" );
45
- $this->prefix = $this->options->prefix;
46
- $this->db->prefix = $this->options->databasePrefix;
47
-
48
- $this->getTables();
49
-
50
- // Fix current step
51
- if( 0 == $this->options->currentStep ) {
52
- $this->options->currentStep = 0;
53
- }
54
- }
55
-
56
- /**
57
- * Get database object to interact with
58
- */
59
- private function getStagingDB() {
60
- return new \wpdb( $this->options->databaseUser, str_replace( "\\\\", "\\", $this->options->databasePassword ), $this->options->databaseDatabase, $this->options->databaseServer );
61
- }
62
-
63
- /**
64
- * Get a list of tables to copy
65
- */
66
- private function getTables() {
67
- $strings = new Strings();
68
- $this->tables = array();
69
- foreach ( $this->options->tables as $table ) {
70
- $this->tables[] = $this->options->prefix . $strings->str_replace_first( $this->productionDb->prefix, null, $table );
71
- }
72
- // Add extra global tables from main multisite (wpstgx_users and wpstgx_usermeta)
73
- $this->tables[] = $this->options->prefix . $strings->str_replace_first( $this->productionDb->prefix, null, 'users' );
74
- $this->tables[] = $this->options->prefix . $strings->str_replace_first( $this->productionDb->prefix, null, 'usermeta' );
75
- }
76
-
77
- /**
78
- * Calculate Total Steps in This Job and Assign It to $this->options->totalSteps
79
- * @return void
80
- */
81
- protected function calculateTotalSteps() {
82
- $this->options->totalSteps = 22;
83
- }
84
-
85
- /**
86
- * Start Module
87
- * @return object
88
- */
89
- public function start() {
90
- // Execute steps
91
- $this->run();
92
-
93
- // Save option, progress
94
- $this->saveOptions();
95
-
96
- return ( object ) $this->response;
97
- }
98
-
99
- /**
100
- * Execute the Current Step
101
- * Returns false when over threshold limits are hit or when the job is done, true otherwise
102
- * @return bool
103
- */
104
- protected function execute() {
105
- // Fatal error. Let this happen never and break here immediately
106
- if( $this->isRoot() ) {
107
- return false;
108
- }
109
-
110
- // Over limits threshold
111
- if( $this->isOverThreshold() ) {
112
- // Prepare response and save current progress
113
- $this->prepareResponse( false, false );
114
- $this->saveOptions();
115
- return false;
116
- }
117
-
118
- // No more steps, finished
119
- if( $this->isFinished() ) {
120
- $this->prepareResponse( true, false );
121
- return false;
122
- }
123
-
124
- // Execute step
125
- $stepMethodName = "step" . $this->options->currentStep;
126
- if( !$this->{$stepMethodName}() ) {
127
- $this->prepareResponse( false, false );
128
- return false;
129
- }
130
-
131
- // Prepare Response
132
- $this->prepareResponse();
133
-
134
- // Not finished
135
- return true;
136
- }
137
-
138
- /**
139
- * Checks Whether There is Any Job to Execute or Not
140
- * @return bool
141
- */
142
- protected function isFinished() {
143
- return (
144
- !isset( $this->options->isRunning ) ||
145
- $this->options->currentStep > $this->options->totalSteps ||
146
- !method_exists( $this, "step" . $this->options->currentStep )
147
- );
148
- }
149
-
150
- /**
151
- * Check if current operation is done on the root folder or on the live DB
152
- * @return boolean
153
- */
154
- protected function isRoot() {
155
-
156
- // Prefix is the same as the one of live site
157
- // $wpdb = WPStaging::getInstance()->get( "wpdb" );
158
- // if( $wpdb->prefix === $this->prefix ) {
159
- // return true;
160
- // }
161
- // CloneName is empty
162
- $name = ( array ) $this->options->cloneDirectoryName;
163
- if( empty( $name ) ) {
164
- return true;
165
- }
166
-
167
- // Live domain === Staging domain
168
- if( $this->multisiteHomeDomain . $this->options->cloneDirectoryName === $this->multisiteHomeDomain ) {
169
- return true;
170
- }
171
-
172
- return false;
173
- }
174
-
175
- /**
176
- * Check if table exists
177
- * @param string $table
178
- * @return boolean
179
- */
180
- protected function isTable( $table ) {
181
- if( $this->db->get_var( "SHOW TABLES LIKE '{$table}'" ) != $table ) {
182
- $this->log( "Table {$table} does not exist", Logger::TYPE_ERROR );
183
- return false;
184
- }
185
- return true;
186
- }
187
-
188
- /**
189
- * Copy wp-config.php from the staging site if it is located outside of root one level up or
190
- * copy default wp-config.php if production site uses bedrock or any other boilerplate solution that stores wp default config data elsewhere.
191
- * @return boolean
192
- */
193
- protected function step0() {
194
- $this->log( "Preparing Data Step0: Copy wp-config.php file", Logger::TYPE_INFO );
195
-
196
- $dir = trailingslashit( dirname( ABSPATH ) );
197
-
198
- $source = $dir . 'wp-config.php';
199
-
200
- $destination = $this->options->destinationDir . 'wp-config.php';
201
-
202
- // Check if there is already a valid wp-config.php in root of staging site
203
- if( $this->isValidWpConfig( $destination ) ) {
204
- $this->log( "Preparing Data Step0: Found wp-config.php file in folder {$destination}", Logger::TYPE_INFO );
205
- return true;
206
- }
207
-
208
- // Check if there is a valid wp-config.php outside root of wp production site
209
- if( $this->isValidWpConfig( $source ) ) {
210
- // Copy it to staging site
211
- if( $this->copy( $source, $destination ) ) {
212
- $this->log( "Preparing Data Step0: Copy wp-config.php file from source {$source} to {$destination}", Logger::TYPE_INFO );
213
- return true;
214
- }
215
- }
216
-
217
- // No valid wp-config.php found so let's copy wp stagings default wp-config.php to staging site
218
- $source = WPSTG_PLUGIN_DIR . "apps/Backend/helpers/wp-config.php";
219
-
220
- $this->log( "Preparing Data Step0: Copy default wp-config.php file from source {$source} to {$destination}", Logger::TYPE_INFO );
221
-
222
- if( $this->copy( $source, $destination ) ) {
223
- // add missing db credentials to wp-config.php
224
- if( !$this->alterWpConfig( $destination ) ) {
225
- $this->log( "Preparing Data Step0: Can not alter db credentials in wp-config.php", Logger::TYPE_INFO );
226
- return false;
227
- }
228
- }
229
-
230
- $this->log( "Preparing Data Step0: Successful", Logger::TYPE_INFO );
231
- return true;
232
- }
233
-
234
- /**
235
- * Copy files with symlink support
236
- * @param type $source
237
- * @param type $destination
238
- * @return boolean
239
- */
240
- protected function copy( $source, $destination ) {
241
- // Copy symbolic link
242
- if( is_link( $source ) ) {
243
- $this->log( "Preparing Data: Symbolic link found...", Logger::TYPE_INFO );
244
- if( !@copy( readlink( $source ), $destination ) ) {
245
- $errors = error_get_last();
246
- $this->log( "Preparing Data: Failed to copy {$source} Error: {$errors['message']} {$source} -> {$destination}", Logger::TYPE_ERROR );
247
- return false;
248
- }
249
- }
250
-
251
- // Copy file
252
- if( !@copy( $source, $destination ) ) {
253
- $errors = error_get_last();
254
- $this->log( "Preparing Data Step0: Failed to copy {$source}! Error: {$errors['message']} {$source} -> {$destination}", Logger::TYPE_ERROR );
255
- return false;
256
- }
257
-
258
- return true;
259
- }
260
-
261
- /**
262
- * Make sure wp-config.php contains correct db credentials
263
- * @param type $source
264
- * @return boolean
265
- */
266
- protected function alterWpConfig( $source ) {
267
- $this->log( "Preparing Data: Alter wp-config.php", Logger::TYPE_INFO );
268
-
269
- $content = file_get_contents( $source );
270
-
271
- if( false === ($content = file_get_contents( $source )) ) {
272
- return false;
273
- }
274
-
275
- $search = "// ** MySQL settings ** //";
276
-
277
- $replace = "// ** MySQL settings ** //\r\n
278
- define( 'DB_NAME', '" . DB_NAME . "' );\r\n
279
- /** MySQL database username */\r\n
280
- define( 'DB_USER', '" . DB_USER . "' );\r\n
281
- /** MySQL database password */\r\n
282
- define( 'DB_PASSWORD', '" . DB_PASSWORD . "' );\r\n
283
- /** MySQL hostname */\r\n
284
- define( 'DB_HOST', '" . DB_HOST . "' );\r\n
285
- /** Database Charset to use in creating database tables. */\r\n
286
- define( 'DB_CHARSET', '" . DB_CHARSET . "' );\r\n
287
- /** The Database Collate type. Don't change this if in doubt. */\r\n
288
- define( 'DB_COLLATE', '" . DB_COLLATE . "' );\r\n";
289
-
290
- $content = str_replace( $search, $replace, $content );
291
-
292
- if( false === @wpstg_put_contents( $source, $content ) ) {
293
- $this->log( "Preparing Data: Can't save wp-config.php", Logger::TYPE_ERROR );
294
- return false;
295
- }
296
-
297
- return true;
298
- }
299
-
300
- /**
301
- * Check if wp-config.php contains important constants
302
- * @param type $source
303
- * @return boolean
304
- */
305
- protected function isValidWpConfig( $source ) {
306
-
307
- if( !is_file( $source ) && !is_link( $source ) ) {
308
- $this->log( "Preparing Data Step0: Can not find {$source}", Logger::TYPE_INFO );
309
- return false;
310
- }
311
-
312
- $content = file_get_contents( $source );
313
-
314
- if( false === ($content = file_get_contents( $source )) ) {
315
- $this->log( "Preparing Data Step0: Can not read {$source}", Logger::TYPE_INFO );
316
- return false;
317
- }
318
-
319
- // Get DB_NAME from wp-config.php
320
- preg_match( "/define\s*\(\s*['\"]DB_NAME['\"]\s*,\s*(.*)\s*\);/", $content, $matches );
321
-
322
- if( empty( $matches[1] ) ) {
323
- $this->log( "Preparing Data Step0: Can not find DB_NAME in wp-config.php", Logger::TYPE_INFO );
324
- return false;
325
- }
326
-
327
- // Get DB_USER from wp-config.php
328
- preg_match( "/define\s*\(\s*['\"]DB_USER['\"]\s*,\s*(.*)\s*\);/", $content, $matches );
329
-
330
- if( empty( $matches[1] ) ) {
331
- $this->log( "Preparing Data Step0: Can not find DB_USER in wp-config.php", Logger::TYPE_INFO );
332
- return false;
333
- }
334
-
335
- // Get DB_PASSWORD from wp-config.php
336
- preg_match( "/define\s*\(\s*['\"]DB_PASSWORD['\"]\s*,\s*(.*)\s*\);/", $content, $matches );
337
-
338
- if( empty( $matches[1] ) ) {
339
- $this->log( "Preparing Data Step0: Can not find DB_PASSWORD in wp-config.php", Logger::TYPE_INFO );
340
- return false;
341
- }
342
-
343
- // Get DB_HOST from wp-config.php
344
- preg_match( "/define\s*\(\s*['\"]DB_HOST['\"]\s*,\s*(.*)\s*\);/", $content, $matches );
345
-
346
- if( empty( $matches[1] ) ) {
347
- $this->log( "Preparing Data Step0: Can not find DB_HOST in wp-config.php", Logger::TYPE_INFO );
348
- return false;
349
- }
350
- return true;
351
- }
352
-
353
- /**
354
- * Replace "siteurl" and "home"
355
- * @return bool
356
- */
357
- protected function step1() {
358
- $this->log( "Preparing Data Step1: Updating siteurl and homeurl in {$this->prefix}options {$this->db->last_error}", Logger::TYPE_INFO );
359
-
360
- // Skip - Table does not exist
361
- if( false === $this->isTable( $this->prefix . 'options' ) ) {
362
- return true;
363
- }
364
- // Skip - Table is not selected or updated
365
- if( !in_array( $this->prefix . 'options', $this->tables ) ) {
366
- $this->log( "Preparing Data Step1: Skipping" );
367
- return true;
368
- }
369
-
370
- $this->log( "Preparing Data Step1: Updating siteurl and homeurl to " . $this->getStagingSiteUrl() );
371
- // Replace URLs
372
- $result = $this->db->query(
373
- $this->db->prepare(
374
- "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'siteurl' or option_name='home'", $this->getStagingSiteUrl()
375
- )
376
- );
377
-
378
-
379
-
380
- // All good
381
- if( $result ) {
382
- return true;
383
- }
384
-
385
- $this->log( "Preparing Data Step1: Skip updating siteurl and homeurl in {$this->prefix}options. Probably already did! {$this->db->last_error}", Logger::TYPE_WARNING );
386
- return true;
387
- }
388
-
389
- /**
390
- * Update "wpstg_is_staging_site"
391
- * @return bool
392
- */
393
- protected function step2() {
394
-
395
- $this->log( "Preparing Data Step2: Updating row wpstg_is_staging_site in {$this->prefix}options {$this->db->last_error}" );
396
-
397
- // Skip - Table does not exist
398
- if( false === $this->isTable( $this->prefix . 'options' ) ) {
399
- $this->log( "Preparing Data Step2: Skipping" );
400
- return true;
401
- }
402
- // Skip - Table is not selected or updated
403
- if( !in_array( $this->prefix . 'options', $this->tables ) ) {
404
- $this->log( "Preparing Data Step2: Skipping" );
405
- return true;
406
- }
407
-
408
- $result = $this->db->query(
409
- $this->db->prepare(
410
- "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'wpstg_is_staging_site'", "true"
411
- )
412
- );
413
-
414
- // No errors but no option name such as wpstg_is_staging_site
415
- if( '' === $this->db->last_error && 0 == $result ) {
416
- $result = $this->db->query(
417
- $this->db->prepare(
418
- "INSERT INTO {$this->prefix}options (option_name,option_value) VALUES ('wpstg_is_staging_site',%s)", "true"
419
- )
420
- );
421
- }
422
-
423
- // All good
424
- if( $result ) {
425
- return true;
426
- }
427
-
428
- $this->log( "Preparing Data Step2: Failed to update wpstg_is_staging_site in {$this->prefix}options {$this->db->last_error}", Logger::TYPE_ERROR );
429
- return false;
430
- }
431
-
432
- /**
433
- * Update rewrite_rules
434
- * @return bool
435
- */
436
- protected function step3() {
437
-
438
- $this->log( "Preparing Data Step3: Updating rewrite_rules in {$this->prefix}options {$this->db->last_error}" );
439
-
440
- // Keep Permalinks
441
- if( isset( $this->settings->keepPermalinks ) && $this->settings->keepPermalinks === "1" ) {
442
- $this->log( "Preparing Data Step3: Skipping" );
443
- return true;
444
- }
445
-
446
- // Skip - Table does not exist
447
- if( false === $this->isTable( $this->prefix . 'options' ) ) {
448
- return true;
449
- }
450
-
451
- // Skip - Table is not selected or updated
452
- if( !in_array( $this->prefix . 'options', $this->tables ) ) {
453
- $this->log( "Preparing Data Step3: Skipping" );
454
- return true;
455
- }
456
-
457
- $result = $this->db->query(
458
- $this->db->prepare(
459
- "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'rewrite_rules'", ' '
460
- )
461
- );
462
-
463
- // All good
464
- if( $result ) {
465
- return true;
466
- }
467
-
468
- //$this->log( "Preparing Data Step3: Failed to update rewrite_rules in {$this->prefix}options {$this->db->last_error}", Logger::TYPE_ERROR );
469
- return true;
470
- }
471
-
472
- /**
473
- * Update Table Prefix in wp_usermeta
474
- * @return bool
475
- */
476
- protected function step4() {
477
- $this->log( "Preparing Data Step4: Updating db prefix in {$this->prefix}usermeta. " );
478
-
479
- // Skip - Table does not exist
480
- if( false === $this->isTable( $this->prefix . 'usermeta' ) ) {
481
- return true;
482
- }
483
-
484
- // Skip - Table is not selected or updated
485
- if( !in_array( $this->prefix . 'usermeta', $this->tables ) ) {
486
- $this->log( "Preparing Data Step4: Skipping" );
487
- return true;
488
- }
489
-
490
- // Change the base table prefix of the main network site
491
- $this->debugLog( "Preparing Data Step4: UPDATE {$this->prefix}usermeta SET meta_key = replace(meta_key, {$this->productionDb->base_prefix}, {$this->prefix}) WHERE meta_key LIKE {$this->productionDb->base_prefix}_%" );
492
- $update = $this->db->query(
493
- $this->db->prepare(
494
- "UPDATE {$this->prefix}usermeta SET meta_key = replace(meta_key, %s, %s) WHERE meta_key LIKE %s", $this->productionDb->base_prefix, $this->prefix, $this->productionDb->base_prefix . "_%"
495
- )
496
- );
497
-
498
- if( false === $update ) {
499
- $this->log( "Preparing Data Step4: Skip updating {$this->prefix}usermeta meta_key database base_prefix; {$this->db->last_error}", Logger::TYPE_INFO );
500
- }
501
-
502
- // Change the table prefix of the child network site
503
- $this->debugLog( "Preparing Data Step4: UPDATE {$this->prefix}usermeta SET meta_key = replace(meta_key, {$this->db->prefix}, {$this->prefix}) WHERE meta_key LIKE {$this->db->prefix}_%" );
504
- $update = $this->db->query(
505
- $this->db->prepare(
506
- "UPDATE {$this->prefix}usermeta SET meta_key = replace(meta_key, %s, %s) WHERE meta_key LIKE %s", $this->db->prefix, $this->prefix, $this->db->prefix . "_%"
507
- )
508
- );
509
-
510
- if( false === $update ) {
511
- $this->log( "Preparing Data Step4: Failed to update {$this->prefix}usermeta meta_key database table prefixes; {$this->db->last_error}", Logger::TYPE_ERROR );
512
- $this->returnException( "Data Crunching Step 4: Failed to update {$this->prefix}usermeta meta_key database table prefixes; {$this->db->last_error}" );
513
- return false;
514
- }
515
- return true;
516
- }
517
-
518
- /**
519
- * Update $table_prefix in wp-config.php
520
- * @return bool
521
- */
522
- protected function step5() {
523
- $path = $this->options->destinationDir . "wp-config.php";
524
-
525
- $this->log( "Preparing Data Step5: Updating table_prefix in {$path} to " . $this->prefix );
526
- if( false === ($content = file_get_contents( $path )) ) {
527
- $this->log( "Preparing Data Step5: Failed to update table_prefix in {$path}. Can't read contents", Logger::TYPE_ERROR );
528
- return false;
529
- }
530
-
531
- // Replace table prefix
532
- $pattern = '/\$table_prefix\s*=\s*(.*).*/';
533
- $replacement = '$table_prefix = \'' . $this->prefix . '\'; // Changed by WP Staging';
534
- $content = preg_replace( $pattern, $replacement, $content );
535
-
536
- if( null === $content ) {
537
- $this->log( "Preparing Data Step5: Failed to update table_prefix in {$path}. Can't read contents", Logger::TYPE_ERROR );
538
- return false;
539
- }
540
-
541
- // Replace URLs
542
- $content = str_replace( $this->multisiteHomeDomain, $this->getStagingSiteUrl(), $content );
543
-
544
- if( false === @wpstg_put_contents( $path, $content ) ) {
545
- $this->log( "Preparing Data Step5: Failed to update $table_prefix in {$path} to " . $this->prefix . ". Can't save contents", Logger::TYPE_ERROR );
546
- return false;
547
- }
548
-
549
- return true;
550
- }
551
-
552
- /**
553
- * Reset index.php to original file
554
- * This is needed if live site is located in subfolder
555
- * Check first if main wordpress is used in subfolder and index.php in parent directory
556
- * @see: https://codex.wordpress.org/Giving_WordPress_Its_Own_Directory
557
- * @return bool
558
- */
559
- protected function step6() {
560
-
561
- if( !$this->isSubDir() ) {
562
- $this->debugLog( "Preparing Data Step6: WP installation is not in a subdirectory! All good, skipping this step" );
563
- return true;
564
- }
565
-
566
- $path = $this->options->destinationDir . "index.php";
567
-
568
- if( false === ($content = file_get_contents( $path )) ) {
569
- $this->log( "Preparing Data Step6: Failed to reset {$path} for sub directory; can't read contents", Logger::TYPE_ERROR );
570
- return false;
571
- }
572
-
573
-
574
- if( !preg_match( "/(require(.*)wp-blog-header.php' \);)/", $content, $matches ) ) {
575
- $this->log(
576
- "Preparing Data Step6: Failed to reset index.php for sub directory; wp-blog-header.php is missing", Logger::TYPE_ERROR
577
- );
578
- return false;
579
- }
580
- $this->log( "Preparing Data: WP installation is in a subdirectory. Progressing..." );
581
-
582
- $pattern = "/require(.*) dirname(.*) __FILE__ (.*) \. '(.*)wp-blog-header.php'(.*);.*/";
583
-
584
- $replace = "require( dirname( __FILE__ ) . '/wp-blog-header.php' ); // " . $matches[0];
585
- //$replace.= " // Changed by WP-Staging";
586
-
587
-
588
-
589
- if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
590
- $this->log( "Preparing Data: Failed to reset index.php for sub directory; replacement failed", Logger::TYPE_ERROR );
591
- return false;
592
- }
593
-
594
- if( false === @wpstg_put_contents( $path, $content ) ) {
595
- $this->log( "Preparing Data: Failed to reset index.php for sub directory; can't save contents", Logger::TYPE_ERROR );
596
- return false;
597
- }
598
- $this->Log( "Preparing Data Step6: Finished successfully" );
599
- return true;
600
- }
601
-
602
- /**
603
- * Update wpstg_rmpermalinks_executed
604
- * @return bool
605
- */
606
- protected function step7() {
607
-
608
- $this->log( "Preparing Data Step7: Updating wpstg_rmpermalinks_executed in {$this->prefix}options {$this->db->last_error}" );
609
-
610
- // Skip - Table does not exist
611
- if( false === $this->isTable( $this->prefix . 'options' ) ) {
612
- return true;
613
- }
614
-
615
- // Skip - Table is not selected or updated
616
- if( !in_array( $this->prefix . 'options', $this->tables ) ) {
617
- $this->log( "Preparing Data Step7: Skipping" );
618
- return true;
619
- }
620
-
621
- $result = $this->db->query(
622
- $this->db->prepare(
623
- "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'wpstg_rmpermalinks_executed'", ' '
624
- )
625
- );
626
-
627
- $this->Log( "Preparing Data Step7: Finished successfully" );
628
- return true;
629
- }
630
-
631
- /**
632
- * Update permalink_structure
633
- * @return bool
634
- */
635
- protected function step8() {
636
-
637
- $this->log( "Preparing Data Step8: Updating permalink_structure in {$this->prefix}options {$this->db->last_error}" );
638
-
639
- // Keep Permalinks
640
- if( isset( $this->settings->keepPermalinks ) && $this->settings->keepPermalinks === "1" ) {
641
- $this->log( "Preparing Data Step8: Skipping" );
642
- return true;
643
- }
644
-
645
- // Skip - Table does not exist
646
- if( false === $this->isTable( $this->prefix . 'options' ) ) {
647
- return true;
648
- }
649
-
650
- // Skip - Table is not selected or updated
651
- if( !in_array( $this->prefix . 'options', $this->tables ) ) {
652
- $this->log( "Preparing Data Step8: Skipping" );
653
- return true;
654
- }
655
-
656
- $result = $this->db->query(
657
- $this->db->prepare(
658
- "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'permalink_structure'", ' '
659
- )
660
- );
661
-
662
- // All good
663
- if( $result ) {
664
- $this->Log( "Preparing Data Step8: Finished successfully" );
665
- return true;
666
- }
667
-
668
- $this->log( "Failed to update permalink_structure in {$this->prefix}options {$this->db->last_error}", Logger::TYPE_ERROR );
669
- return true;
670
- }
671
-
672
- /**
673
- * Update blog_public option to not allow staging site to be indexed by search engines
674
- * @return bool
675
- */
676
- protected function step9() {
677
-
678
- $this->log( "Preparing Data Step9: Set staging site to noindex" );
679
-
680
- if( false === $this->isTable( $this->prefix . 'options' ) ) {
681
- return true;
682
- }
683
-
684
- // Skip - Table is not selected or updated
685
- if( !in_array( $this->prefix . 'options', $this->tables ) ) {
686
- $this->log( "Preparing Data Step9: Skipping" );
687
- return true;
688
- }
689
-
690
- $result = $this->db->query(
691
- $this->db->prepare(
692
- "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'blog_public'", '0'
693
- )
694
- );
695
-
696
- // All good
697
- if( $result ) {
698
- $this->Log( "Preparing Data Step9: Finished successfully" );
699
- return true;
700
- }
701
-
702
- $this->log( "Can not update staging site to noindex. Possible already done!", Logger::TYPE_WARNING );
703
- return true;
704
- }
705
-
706
- /**
707
- * Update WP_HOME in wp-config.php
708
- * @return bool
709
- */
710
- protected function step10() {
711
- $path = $this->options->destinationDir . "wp-config.php";
712
-
713
- $this->log( "Preparing Data Step10: Updating WP_HOME in wp-config.php to " . $this->getStagingSiteUrl() );
714
-
715
- if( false === ($content = file_get_contents( $path )) ) {
716
- $this->log( "Preparing Data Step10: Failed to update WP_HOME in wp-config.php. Can't read wp-config.php", Logger::TYPE_ERROR );
717
- return false;
718
- }
719
-
720
-
721
- // Get WP_HOME from wp-config.php
722
- preg_match( "/define\s*\(\s*['\"]WP_HOME['\"]\s*,\s*(.*)\s*\);/", $content, $matches );
723
-
724
- if( !empty( $matches[1] ) ) {
725
- $matches[1];
726
-
727
- $pattern = "/define\s*\(\s*['\"]WP_HOME['\"]\s*,\s*(.*)\s*\);.*/";
728
-
729
- $replace = "define('WP_HOME','" . $this->getStagingSiteUrl() . "'); // " . $matches[1];
730
- //$replace .= " // Changed by WP-Staging";
731
-
732
- if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
733
- $this->log( "Preparing Data: Failed to update WP_HOME", Logger::TYPE_ERROR );
734
- return false;
735
- }
736
- } else {
737
- $this->log( "Preparing Data Step10: WP_HOME not defined in wp-config.php. Skipping this step." );
738
- }
739
-
740
- if( false === @wpstg_put_contents( $path, $content ) ) {
741
- $this->log( "Preparing Data Step10: Failed to update WP_HOME. Can't save contents", Logger::TYPE_ERROR );
742
- return false;
743
- }
744
- $this->Log( "Preparing Data Step 10: Finished successfully" );
745
- return true;
746
- }
747
-
748
- /**
749
- * Update WP_SITEURL in wp-config.php
750
- * @return bool
751
- */
752
- protected function step11() {
753
- $path = $this->options->destinationDir . "wp-config.php";
754
-
755
- $this->log( "Preparing Data Step11: Updating WP_SITEURL in wp-config.php to " . $this->getStagingSiteUrl() );
756
-
757
- if( false === ($content = file_get_contents( $path )) ) {
758
- $this->log( "Preparing Data Step11: Failed to update WP_SITEURL in wp-config.php. Can't read wp-config.php", Logger::TYPE_ERROR );
759
- return false;
760
- }
761
-
762
-
763
- // Get WP_SITEURL from wp-config.php
764
- preg_match( "/define\s*\(\s*['\"]WP_SITEURL['\"]\s*,\s*(.*)\s*\);/", $content, $matches );
765
-
766
- if( !empty( $matches[1] ) ) {
767
- $matches[1];
768
-
769
- $pattern = "/define\s*\(\s*['\"]WP_SITEURL['\"]\s*,\s*(.*)\s*\);.*/";
770
-
771
- $replace = "define('WP_SITEURL','" . $this->getStagingSiteUrl() . "'); // " . $matches[1];
772
- //$replace .= " // Changed by WP-Staging";
773
-
774
- if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
775
- $this->log( "Preparing Data Step11: Failed to update WP_SITEURL", Logger::TYPE_ERROR );
776
- return false;
777
- }
778
- } else {
779
- $this->log( "Preparing Data Step11: WP_SITEURL not defined in wp-config.php. Skipping this step." );
780
- }
781
-
782
-
783
- if( false === @wpstg_put_contents( $path, $content ) ) {
784
- $this->log( "Preparing Data Step11: Failed to update WP_SITEURL. Can't save contents", Logger::TYPE_ERROR );
785
- return false;
786
- }
787
- $this->Log( "Preparing Data Step 11: Finished successfully" );
788
- return true;
789
- }
790
-
791
- /**
792
- * Update WP_ALLOW_MULTISITE constant in wp-config.php
793
- * @return bool
794
- */
795
- protected function step12() {
796
- $path = $this->options->destinationDir . "wp-config.php";
797
-
798
- $this->log( "Preparing Data Step12: Updating WP_ALLOW_MULTISITE in wp-config.php to false" );
799
-
800
- if( false === ($content = file_get_contents( $path )) ) {
801
- $this->log( "Preparing Data Step12: Failed to update WP_ALLOW_MULTISITE in wp-config.php. Can't read wp-config.php", Logger::TYPE_ERROR );
802
- return false;
803
- }
804
-
805
-
806
- // Get WP_SITEURL from wp-config.php
807
- preg_match( "/define\s*\(\s*['\"]WP_ALLOW_MULTISITE['\"]\s*,\s*(.*)\s*\);/", $content, $matches );
808
-
809
- if( !empty( $matches[1] ) ) {
810
- $matches[1];
811
-
812
- $pattern = "/define\s*\(\s*['\"]WP_ALLOW_MULTISITE['\"]\s*,\s*(.*)\s*\);.*/";
813
-
814
- $replace = "define('WP_ALLOW_MULTISITE',false); // " . $matches[1];
815
- //$replace .= " // Changed by WP-Staging";
816
-
817
- if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
818
- $this->log( "Preparing Data Step12: Failed to update WP_ALLOW_MULTISITE", Logger::TYPE_ERROR );
819
- return false;
820
- }
821
- } else {
822
- $this->log( "Preparing Data Step12: WP_ALLOW_MULTISITE not defined in wp-config.php. Skipping this step." );
823
- }
824
-
825
-
826
- if( false === @wpstg_put_contents( $path, $content ) ) {
827
- $this->log( "Preparing Data Step12: Failed to update WP_ALLOW_MULTISITE. Can't save contents", Logger::TYPE_ERROR );
828
- return false;
829
- }
830
- $this->Log( "Preparing Data: Finished Step 12 successfully" );
831
- return true;
832
- }
833
-
834
- /**
835
- * Update MULTISITE constant in wp-config.php
836
- * @return bool
837
- */
838
- protected function step13() {
839
- $path = $this->options->destinationDir . "wp-config.php";
840
-
841
- $this->log( "Preparing Data Step13: Updating MULTISITE in wp-config.php to false" );
842
-
843
- if( false === ($content = file_get_contents( $path )) ) {
844
- $this->log( "Preparing Data Step13: Failed to update MULTISITE in wp-config.php. Can't read wp-config.php", Logger::TYPE_ERROR );
845
- return false;
846
- }
847
-
848
-
849
- // Get WP_SITEURL from wp-config.php
850
- preg_match( "/define\s*\(\s*['\"]MULTISITE['\"]\s*,\s*(.*)\s*\);/", $content, $matches );
851
-
852
- if( !empty( $matches[1] ) ) {
853
- $matches[1];
854
-
855
- $pattern = "/define\s*\(\s*['\"]MULTISITE['\"]\s*,\s*(.*)\s*\);.*/";
856
-
857
- $replace = "define('MULTISITE',false); // " . $matches[1];
858
- //$replace .= " // Changed by WP-Staging";
859
-
860
- if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
861
- $this->log( "Preparing Data Step13: Failed to update MULTISITE", Logger::TYPE_ERROR );
862
- return false;
863
- }
864
- } else {
865
- $this->log( "Preparing Data Step13: MULTISITE not defined in wp-config.php. Skipping this step." );
866
- }
867
-
868
-
869
- if( false === @wpstg_put_contents( $path, $content ) ) {
870
- $this->log( "Preparing Data Step13: Failed to update MULTISITE. Can't save contents", Logger::TYPE_ERROR );
871
- return false;
872
- }
873
- $this->Log( "Preparing Data Step13: Finished successfully" );
874
- return true;
875
- }
876
-
877
- /**
878
- * Get active_sitewide_plugins from wp_sitemeta and active_plugins from subsite
879
- * Merge both arrays and copy them to the staging site into active_plugins
880
- */
881
- protected function step14() {
882
-
883
-
884
- $this->log( "Data Crunching Step14: Updating active_plugins" );
885
-
886
- if( false === $this->isTable( $this->prefix . 'options' ) ) {
887
- $this->log( 'Data Crunching Step14: Fatal Error ' . $this->prefix . 'options does not exist' );
888
- $this->returnException( 'Data Crunching Step14: Fatal Error ' . $this->prefix . 'options does not exist' );
889
- return false;
890
- }
891
-
892
- // Skip - Table is not selected or updated
893
- if( !in_array( $this->prefix . 'options', $this->tables ) ) {
894
- $this->log( "Preparing Data Step14: Skipping" );
895
- return true;
896
- }
897
-
898
- // Get active_plugins value from sub site options table
899
- $active_plugins = $this->productionDb->get_var( "SELECT option_value FROM {$this->productionDb->prefix}options WHERE option_name = 'active_plugins' " );
900
-
901
- if( !$active_plugins ) {
902
- $this->log( "Data Crunching Step14: Option active_plugins are empty " );
903
- $active_plugins = array();
904
- }
905
- // Get active_sitewide_plugins value from main multisite wp_sitemeta table
906
- $active_sitewide_plugins = $this->productionDb->get_var( "SELECT meta_value FROM {$this->productionDb->base_prefix}sitemeta WHERE meta_key = 'active_sitewide_plugins' " );
907
-
908
- if( !$active_sitewide_plugins ) {
909
- $this->log( "Data Crunching Step14: Options {$this->productionDb->base_prefix}active_sitewide_plugins is empty " );
910
- $active_sitewide_plugins = array();
911
- }
912
-
913
- $active_sitewide_plugins = unserialize( $active_sitewide_plugins );
914
- $active_plugins = unserialize( $active_plugins );
915
-
916
- $all_plugins = array_merge( $active_plugins, array_keys( $active_sitewide_plugins ) );
917
-
918
- sort( $all_plugins );
919
-
920
-
921
- // Update active_plugins
922
- $update = $this->db->query(
923
- "UPDATE {$this->prefix}options SET option_value = '" . serialize( $all_plugins ) . "' WHERE option_name = 'active_plugins'"
924
- );
925
-
926
- if( false === $update ) {
927
- $this->log( "Data Crunching Step14: Can not update option active_plugins in {$this->prefix}options", Logger::TYPE_WARNING );
928
- return false;
929
- }
930
-
931
- $this->log( "Data Crunching Step14: Successful!" );
932
- return true;
933
- }
934
-
935
- /**
936
- * Update Table Prefix in wp_options
937
- * @return bool
938
- */
939
- protected function step15() {
940
- $this->log( "Preparing Data Step15: Updating db prefix in {$this->prefix}options." );
941
-
942
- // Skip - Table does not exist
943
- if( false === $this->isTable( $this->prefix . 'options' ) ) {
944
- return true;
945
- }
946
-
947
- // Skip - Table is not selected or updated
948
- if( !in_array( $this->prefix . 'options', $this->tables ) ) {
949
- $this->log( "Preparing Data Step4: Skipping" );
950
- return true;
951
- }
952
-
953
-
954
- $this->log( "Updating db option_names in {$this->prefix}options. " );
955
-
956
- // Filter the rows below. Do not update them!
957
- $filters = array(
958
- 'wp_mail_smtp',
959
- 'wp_mail_smtp_version',
960
- 'wp_mail_smtp_debug',
961
- );
962
-
963
- $filters = apply_filters( 'wpstg_data_excl_rows', $filters );
964
-
965
- $where = "";
966
- foreach ( $filters as $filter ) {
967
- $where .= " AND option_name <> '" . $filter . "'";
968
- }
969
-
970
- $updateOptions = $this->db->query(
971
- $this->db->prepare(
972
- "UPDATE IGNORE {$this->prefix}options SET option_name= replace(option_name, %s, %s) WHERE option_name LIKE %s" . $where, $this->productionDb->prefix, $this->prefix, $this->productionDb->prefix . "_%"
973
- )
974
- );
975
-
976
- if( false === $updateOptions ) {
977
- $this->log( "Preparing Data Step15: Failed to update db option_names in {$this->prefix}options. Error: {$this->db->last_error}", Logger::TYPE_WARNING );
978
- //$this->returnException( "Data Crunching Step 15: Failed to update db option_names in {$this->prefix}options. Error: {$this->db->last_error}" );
979
- return true;
980
- }
981
-
982
-
983
- return true;
984
- }
985
-
986
- /**
987
- * Change upload_path in wp_options (if it is defined)
988
- * @return bool
989
- */
990
- protected function step16() {
991
- $this->log( "Preparing Data Step16: Updating upload_path {$this->prefix}options." );
992
-
993
- // Skip - Table does not exist
994
- if( false === $this->isTable( $this->prefix . 'options' ) ) {
995
- return true;
996
- }
997
-
998
- $newUploadPath = $this->getNewUploadPath();
999
-
1000
- if( false === $newUploadPath ) {
1001
- $this->log( "Preparing Data Step16: Skipping" );
1002
- return true;
1003
- }
1004
-
1005
- // Skip - Table is not selected or updated
1006
- if( !in_array( $this->prefix . 'options', $this->tables ) ) {
1007
- $this->log( "Preparing Data Step16: Skipping" );
1008
- return true;
1009
- }
1010
-
1011
- $error = isset( $this->db->last_error ) ? 'Last error: ' . $this->db->last_error : '';
1012
-
1013
- $this->log( "Updating upload_path in {$this->prefix}options. {$error}" );
1014
-
1015
- $updateOptions = $this->db->query(
1016
- $this->db->prepare(
1017
- "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'upload_path'", $newUploadPath
1018
- )
1019
- );
1020
-
1021
- if( false === $updateOptions ) {
1022
- $this->log( "Preparing Data Step16: Failed to update upload_path in {$this->prefix}options. {$error}", Logger::TYPE_ERROR );
1023
- return true;
1024
- }
1025
- $this->Log( "Preparing Data: Finished Step 16 successfully" );
1026
- return true;
1027
- }
1028
-
1029
- /**
1030
- * Update WP_CACHE in wp-config.php
1031
- * @return bool
1032
- */
1033
- protected function step17() {
1034
- $path = $this->options->destinationDir . "wp-config.php";
1035
-
1036
- $this->log( "Preparing Data Step17: Set WP_CACHE in wp-config.php to false" );
1037
-
1038
- if( false === ($content = file_get_contents( $path )) ) {
1039
- $this->log( "Preparing Data Step17: Failed to update WP_CACHE in wp-config.php. Can't read wp-config.php", Logger::TYPE_ERROR );
1040
- return false;
1041
- }
1042
-
1043
-
1044
- // Get WP_CACHE from wp-config.php
1045
- preg_match( "/define\s*\(\s*['\"]WP_CACHE['\"]\s*,\s*(.*)\s*\);/", $content, $matches );
1046
-
1047
- if( !empty( $matches[1] ) ) {
1048
- $matches[1];
1049
-
1050
- $pattern = "/define\s*\(\s*['\"]WP_CACHE['\"]\s*,\s*(.*)\s*\);.*/";
1051
-
1052
- $replace = "define('WP_CACHE',false); // " . $matches[1];
1053
- //$replace.= " // Changed by WP-Staging";
1054
-
1055
- if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
1056
- $this->log( "Preparing Data: Failed to change WP_CACHE", Logger::TYPE_ERROR );
1057
- return false;
1058
- }
1059
- } else {
1060
- $this->log( "Preparing Data Step17: WP_CACHE not defined in wp-config.php. Skipping this step." );
1061
- }
1062
-
1063
- if( false === @wpstg_put_contents( $path, $content ) ) {
1064
- $this->log( "Preparing Data Step17: Failed to update WP_CACHE. Can't save contents", Logger::TYPE_ERROR );
1065
- return false;
1066
- }
1067
- $this->Log( "Preparing Data: Finished Step 17 successfully" );
1068
- return true;
1069
- }
1070
-
1071
- /**
1072
- * Add UPLOADS constant in wp-config.php or change it to correct destination (multisite type /sites/2/)
1073
- * @return bool
1074
- */
1075
- protected function step18() {
1076
- $path = $this->options->destinationDir . "wp-config.php";
1077
- $this->log( "Preparing Data Step18: Update UPLOADS constant in wp-config.php" );
1078
- if( false === ($content = file_get_contents( $path )) ) {
1079
- $this->log( "Preparing Data Step18: Failed to get UPLOADS in wp-config.php. Can't read wp-config.php", Logger::TYPE_ERROR );
1080
- return false;
1081
- }
1082
- // Get UPLOADS from wp-config.php if there is already one
1083
- preg_match( "/define\s*\(\s*['\"]UPLOADS['\"]\s*,\s*(.*)\s*\);/", $content, $matches );
1084
- $uploadFolder = $this->getMultisiteUploadFolder();
1085
- if( !empty( $matches[0] ) ) {
1086
- $pattern = "/define\s*\(\s*'UPLOADS'\s*,\s*(.*)\s*\);/";
1087
-
1088
- $replace = "define('UPLOADS', '" . $uploadFolder . "');";
1089
- $this->log( "Preparing Data Step18: Change UPLOADS constant in wp-config.php to {$uploadFolder}." );
1090
- if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
1091
- $this->log( "Preparing Data Step 18: Failed to change UPLOADS", Logger::TYPE_ERROR );
1092
- return false;
1093
- }
1094
- } else {
1095
- $this->log( "Preparing Data Step18: UPLOADS not defined in wp-config.php. Creating new entry." );
1096
- // Find ABSPATH and add UPLOAD constant above
1097
- preg_match( "/if\s*\(\s*\s*!\s*defined\s*\(\s*['\"]ABSPATH['\"]\s*(.*)\s*\)\s*\)/", $content, $matches );
1098
- if( !empty( $matches[0] ) ) {
1099
- $matches[0];
1100
- $pattern = "/if\s*\(\s*\s*!\s*defined\s*\(\s*['\"]ABSPATH['\"]\s*(.*)\s*\)\s*\)/";
1101
- $replace = "define('UPLOADS', '" . $uploadFolder . "'); \n" .
1102
- "if ( ! defined( 'ABSPATH' ) )";
1103
- $this->log( "Preparing Data Step18: Change UPLOADS constant in wp-config.php to {$uploadFolder}." );
1104
- if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
1105
- $this->log( "Preparing Data Step 18: Failed to change UPLOADS", Logger::TYPE_ERROR );
1106
- return false;
1107
- }
1108
- } else {
1109
- $this->log( "Preparing Data Step 18: Can not add UPLOAD constant to wp-config.php. Can not find free position to add it.", Logger::TYPE_ERROR );
1110
- }
1111
- }
1112
- if( false === @wpstg_put_contents( $path, $content ) ) {
1113
- $this->log( "Preparing Data Step18: Failed to update UPLOADS. Can't save contents", Logger::TYPE_ERROR );
1114
- return false;
1115
- }
1116
- $this->Log( "Preparing Data Step18: Finished successfully" );
1117
- return true;
1118
- }
1119
-
1120
- /**
1121
- * Update database credentials in wp-config.php
1122
- * @return bool
1123
- */
1124
- protected function step19() {
1125
- $path = $this->options->destinationDir . "wp-config.php";
1126
-
1127
- $this->log( "Preparing Data Step19: Change database credentials in wp-config.php" );
1128
-
1129
- if( false === ($content = file_get_contents( $path )) ) {
1130
- $this->log( "Preparing Data Step19: Failed to update database credentials in wp-config.php. Can't read wp-config.php", Logger::TYPE_ERROR );
1131
- return false;
1132
- }
1133
-
1134
-
1135
- // Get DB_NAME from wp-config.php
1136
- preg_match( "/define\s*\(\s*['\"]DB_NAME['\"]\s*,\s*(.*)\s*\);/", $content, $matches );
1137
-
1138
- if( !empty( $matches[1] ) ) {
1139
- $matches[1];
1140
-
1141
- $pattern = "/define\s*\(\s*'DB_NAME'\s*,\s*(.*)\s*\);.*/";
1142
-
1143
- $replace = "define('DB_NAME','{$this->options->databaseDatabase}'); // " . $matches[1];
1144
- //$replace.= " // Changed by WP-Staging";
1145
-
1146
- if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
1147
- $this->log( "Preparing Data: Failed to change DB_NAME", Logger::TYPE_ERROR );
1148
- return false;
1149
- }
1150
- } else {
1151
- $this->log( "Preparing Data Step19: DB_NAME not defined in wp-config.php. Skipping this step." );
1152
- }
1153
- // Get DB_USER from wp-config.php
1154
- preg_match( "/define\s*\(\s*['\"]DB_USER['\"]\s*,\s*(.*)\s*\);/", $content, $matches );
1155
-
1156
- if( !empty( $matches[1] ) ) {
1157
- $matches[1];
1158
-
1159
- $pattern = "/define\s*\(\s*['\"]DB_USER['\"]\s*,\s*(.*)\s*\);.*/";
1160
-
1161
- $replace = "define('DB_USER','{$this->options->databaseUser}'); // " . $matches[1];
1162
- //$replace.= " // Changed by WP-Staging";
1163
-
1164
- if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
1165
- $this->log( "Preparing Data: Failed to change DB_USER", Logger::TYPE_ERROR );
1166
- return false;
1167
- }
1168
- } else {
1169
- $this->log( "Preparing Data Step19: DB_USER not defined in wp-config.php. Skipping this step." );
1170
- }
1171
- // Get DB_PASSWORD from wp-config.php
1172
- preg_match( "/define\s*\(\s*['\"]DB_PASSWORD['\"]\s*,\s*(.*)\s*\);/", $content, $matches );
1173
-
1174
- if( !empty( $matches[1] ) ) {
1175
- $matches[1];
1176
-
1177
- $pattern = "/define\s*\(\s*['\"]DB_PASSWORD['\"]\s*,\s*(.*)\s*\);.*/";
1178
-
1179
- $replace = "define('DB_PASSWORD','{$this->options->databasePassword}'); // Changed by WP Staging";
1180
- //$replace.= " // Changed by WP-Staging";
1181
-
1182
- if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
1183
- $this->log( "Preparing Data: Failed to change DB_PASSWORD", Logger::TYPE_ERROR );
1184
- return false;
1185
- }
1186
- } else {
1187
- $this->log( "Preparing Data Step19: DB_PASSWORD not defined in wp-config.php. Skipping this step." );
1188
- }
1189
- // Get DB_HOST from wp-config.php
1190
- preg_match( "/define\s*\(\s*['\"]DB_HOST['\"]\s*,\s*(.*)\s*\);/", $content, $matches );
1191
-
1192
- if( !empty( $matches[1] ) ) {
1193
- $matches[1];
1194
-
1195
- $pattern = "/define\s*\(\s*['\"]DB_HOST['\"]\s*,\s*(.*)\s*\);.*/";
1196
-
1197
- $replace = "define('DB_HOST','{$this->options->databaseServer}'); // " . $matches[1];
1198
- //$replace.= " // Changed by WP-Staging";
1199
-
1200
- if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
1201
- $this->log( "Preparing Data: Failed to change DB_HOST", Logger::TYPE_ERROR );
1202
- return false;
1203
- }
1204
- } else {
1205
- $this->log( "Preparing Data Step19: DB_HOST not defined in wp-config.php. Skipping this step." );
1206
- }
1207
-
1208
-
1209
- if( false === @wpstg_put_contents( $path, $content ) ) {
1210
- $this->log( "Preparing Data Step19: Failed to update database credentials in wp-config.php. Can't save contents", Logger::TYPE_ERROR );
1211
- return false;
1212
- }
1213
- $this->Log( "Preparing Data Step 19: Finished successfully" );
1214
- return true;
1215
- }
1216
-
1217
- /**
1218
- * Save hostname of parent production site in option_name wpstg_connection
1219
- * @return boolean
1220
- */
1221
- protected function step20() {
1222
-
1223
- $table = $this->prefix . 'options';
1224
-
1225
- $siteurl = get_site_url();
1226
-
1227
- $connection = json_encode( array('prodHostname' => $siteurl) );
1228
-
1229
- $data = array(
1230
- 'option_name' => 'wpstg_connection',
1231
- 'option_value' => $connection
1232
- );
1233
-
1234
- $format = array('%s', '%s');
1235
-
1236
- $result = $this->db->replace( $table, $data, $format );
1237
-
1238
- if( false === $result ) {
1239
- $this->Log( "Preparing Data Step20: Could not save {$siteurl} in {$table}", Logger::TYPE_ERROR );
1240
- }
1241
- return true;
1242
- }
1243
-
1244
- /**
1245
- * Add option_name wpstg_execute and set it to true
1246
- * This option is used to determine if the staging website has not been loaded initiall for executing certain custom actions from \WPStaging\initActions()
1247
- * @return boolean
1248
- */
1249
- protected function step21() {
1250
-
1251
- $table = $this->prefix . 'options';
1252
-
1253
- // Skip - Table does not exist
1254
- if( false === $this->isTable( $table ) ) {
1255
- return true;
1256
- }
1257
-
1258
-
1259
- $result = $this->db->query(
1260
- $this->db->prepare(
1261
- "INSERT INTO {$this->prefix}options (option_name,option_value) VALUES ('wpstg_execute',%s) ON DUPLICATE KEY UPDATE option_value = %s", "true", "true"
1262
- )
1263
- );
1264
-
1265
- if( false === $result ) {
1266
- $this->Log( "Preparing Data Step21: Could not save wpstg_execute in {$table}", Logger::TYPE_ERROR );
1267
- }
1268
- return true;
1269
- }
1270
-
1271
- /**
1272
- * Preserve data and prevents data in wp_options from beeing cloned to staging site
1273
- * @return bool
1274
- */
1275
- protected function step22() {
1276
- $this->log( "Preparing Data Step22: Preserve Data in " . $this->prefix . "options" );
1277
-
1278
- // Skip - Table does not exist
1279
- if( false === $this->isTable( $this->prefix . 'options' ) ) {
1280
- return true;
1281
- }
1282
-
1283
- // Skip - Table is not selected or updated
1284
- if( !in_array( $this->prefix . 'options', $this->tables ) ) {
1285
- $this->log( "Preparing Data Step22: Skipped" );
1286
- return true;
1287
- }
1288
-
1289
- $sql = '';
1290
-
1291
- $preserved_option_names = array('wpstg_existing_clones_beta');
1292
-
1293
- $preserved_option_names = apply_filters( 'wpstg_preserved_options_cloning', $preserved_option_names );
1294
- $preserved_options_escaped = esc_sql( $preserved_option_names );
1295
-
1296
- $preserved_options_data = array();
1297
-
1298
- // Get preserved data in wp_options tables
1299
- $table = $this->db->prefix . 'options';
1300
- $preserved_options_data[$this->prefix . 'options'] = $this->db->get_results(
1301
- sprintf(
1302
- "SELECT * FROM `{$table}` WHERE `option_name` IN ('%s')", implode( "','", $preserved_options_escaped )
1303
- ), ARRAY_A
1304
- );
1305
-
1306
- // Create preserved data queries for options tables
1307
- foreach ( $preserved_options_data as $key => $value ) {
1308
- if( false === empty( $value ) ) {
1309
- foreach ( $value as $option ) {
1310
- $sql .= $this->db->prepare(
1311
- "DELETE FROM `{$key}` WHERE `option_name` = %s;\n", $option['option_name']
1312
- );
1313
-
1314
- $sql .= $this->db->prepare(
1315
- "INSERT INTO `{$key}` ( `option_id`, `option_name`, `option_value`, `autoload` ) VALUES ( NULL , %s, %s, %s );\n", $option['option_name'], $option['option_value'], $option['autoload']
1316
- );
1317
- }
1318
- }
1319
- }
1320
-
1321
- $this->debugLog( "Preparing Data Step22: Preserve values " . json_encode( $preserved_options_data ), Logger::TYPE_INFO );
1322
-
1323
- $this->executeSql( $sql );
1324
-
1325
- $this->log( "Preparing Data Step22: Successful!" );
1326
- return true;
1327
- }
1328
-
1329
- /**
1330
- * Execute a batch of sql queries
1331
- * @param string $sqlbatch
1332
- */
1333
- private function executeSql( $sqlbatch ) {
1334
- $queries = array_filter( explode( ";\n", $sqlbatch ) );
1335
-
1336
- foreach ( $queries as $query ) {
1337
- if( false === $this->db->query( $query ) ) {
1338
- $this->log( "Data Crunching Warning: Can not execute query {$query}", Logger::TYPE_WARNING );
1339
- }
1340
- }
1341
- return true;
1342
- }
1343
-
1344
- /**
1345
- * Get relative path to the uploads media folder of multisite e.g.
1346
- * wp-content/uploads/sites/SITEID or old wordpress structure wp-content/blogs.dir/SITEID/files
1347
- * @return boolean
1348
- */
1349
- protected function getMultisiteUploadFolder() {
1350
- $strings = new Strings();
1351
- // Get absolute path to uploads folder
1352
- $uploads = wp_upload_dir();
1353
- $basedir = $uploads['basedir'];
1354
- // Get relative upload path
1355
- $relDir = str_replace( wpstg_replace_windows_directory_separator( ABSPATH ), null, wpstg_replace_windows_directory_separator( $basedir ) );
1356
- return $relDir;
1357
- }
1358
-
1359
- /**
1360
- * Get Upload Path to staging site
1361
- * @return boolean|string
1362
- */
1363
- protected function getNewUploadPath() {
1364
- $uploadPath = get_option( 'upload_path' );
1365
-
1366
- if( !$uploadPath ) {
1367
- return false;
1368
- }
1369
-
1370
- $customSlug = str_replace( wpstg_replace_windows_directory_separator( \WPStaging\WPStaging::getWPpath() ), '', wpstg_replace_windows_directory_separator( $uploadPath ) );
1371
-
1372
- $newUploadPath = wpstg_replace_windows_directory_separator( \WPStaging\WPStaging::getWPpath() ) . $this->options->cloneDirectoryName . '/' . $customSlug;
1373
-
1374
- return $newUploadPath;
1375
- }
1376
-
1377
- /**
1378
- * Return URL to staging site
1379
- * @return string
1380
- */
1381
- protected function getStagingSiteUrl() {
1382
-
1383
- if( !empty( $this->options->cloneHostname ) ) {
1384
- return $this->options->cloneHostname;
1385
- }
1386
-
1387
- if( $this->isSubDir() ) {
1388
- return trailingslashit( $this->multisiteHomeDomain ) . trailingslashit( $this->getSubDir() ) . $this->options->cloneDirectoryName;
1389
- }
1390
-
1391
- // Get the path to the main multisite without appending and trailingslash e.g. wordpress
1392
- $multisitePath = defined( 'PATH_CURRENT_SITE' ) ? PATH_CURRENT_SITE : '/';
1393
- $url = rtrim( $this->multisiteHomeDomain, '/\\' ) . $multisitePath . $this->options->cloneDirectoryName;
1394
-
1395
- return $url;
1396
- }
1397
-
1398
- /**
1399
- * Check if WP is installed in subdir
1400
- * @return boolean
1401
- */
1402
- protected function isSubDir() {
1403
- // Compare names without scheme to bypass cases where siteurl and home have different schemes http / https
1404
- // This is happening much more often than you would expect
1405
- $siteurl = preg_replace( '#^https?://#', '', rtrim( get_option( 'siteurl' ), '/' ) );
1406
- $home = preg_replace( '#^https?://#', '', rtrim( get_option( 'home' ), '/' ) );
1407
-
1408
- if( $home !== $siteurl ) {
1409
- return true;
1410
- }
1411
- return false;
1412
- }
1413
-
1414
- /**
1415
- * Get the install sub directory if WP is installed in sub directory
1416
- * @return string
1417
- */
1418
- protected function getSubDir() {
1419
- $home = get_option( 'home' );
1420
- $siteurl = get_option( 'siteurl' );
1421
-
1422
- if( empty( $home ) || empty( $siteurl ) ) {
1423
- return '';
1424
- }
1425
-
1426
- $dir = str_replace( $home, '', $siteurl );
1427
- return str_replace( '/', '', $dir );
1428
- }
1429
-
1430
- }
1
+ <?php
2
+
3
+ namespace WPStaging\Backend\Modules\Jobs\Multisite;
4
+
5
+ // No Direct Access
6
+ if( !defined( "WPINC" ) ) {
7
+ die;
8
+ }
9
+
10
+ use WPStaging\Utils\Logger;
11
+ use WPStaging\WPStaging;
12
+ use WPStaging\Utils\Helper;
13
+ use WPStaging\Utils\Multisite;
14
+ use WPStaging\Utils\Strings;
15
+ use WPStaging\Backend\Modules\Jobs\JobExecutable;
16
+
17
+ /**
18
+ * Class Data
19
+ * @package WPStaging\Backend\Modules\Jobs
20
+ */
21
+ class DataExternal extends JobExecutable {
22
+
23
+ /**
24
+ * @var \wpdb
25
+ */
26
+ private $db;
27
+
28
+ /**
29
+ * @var string
30
+ */
31
+ private $prefix;
32
+
33
+ /**
34
+ * Tables e.g wpstg3_options
35
+ * @var array
36
+ */
37
+ private $tables;
38
+
39
+ /**
40
+ * Initialize
41
+ */
42
+ public function initialize() {
43
+ $this->db = $this->getStagingDB();
44
+ $this->productionDb = WPStaging::getInstance()->get( "wpdb" );
45
+ $this->prefix = $this->options->prefix;
46
+ $this->db->prefix = $this->options->databasePrefix;
47
+
48
+ $this->getTables();
49
+
50
+ // Fix current step
51
+ if( 0 == $this->options->currentStep ) {
52
+ $this->options->currentStep = 0;
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Get database object to interact with
58
+ */
59
+ private function getStagingDB() {
60
+ return new \wpdb( $this->options->databaseUser, str_replace( "\\\\", "\\", $this->options->databasePassword ), $this->options->databaseDatabase, $this->options->databaseServer );
61
+ }
62
+
63
+ /**
64
+ * Get a list of tables to copy
65
+ */
66
+ private function getTables() {
67
+ $strings = new Strings();
68
+ $this->tables = array();
69
+ foreach ( $this->options->tables as $table ) {
70
+ $this->tables[] = $this->options->prefix . $strings->str_replace_first( $this->productionDb->prefix, null, $table );
71
+ }
72
+ // Add extra global tables from main multisite (wpstgx_users and wpstgx_usermeta)
73
+ $this->tables[] = $this->options->prefix . $strings->str_replace_first( $this->productionDb->prefix, null, 'users' );
74
+ $this->tables[] = $this->options->prefix . $strings->str_replace_first( $this->productionDb->prefix, null, 'usermeta' );
75
+ }
76
+
77
+ /**
78
+ * Calculate Total Steps in This Job and Assign It to $this->options->totalSteps
79
+ * @return void
80
+ */
81
+ protected function calculateTotalSteps() {
82
+ $this->options->totalSteps = 22;
83
+ }
84
+
85
+ /**
86
+ * Start Module
87
+ * @return object
88
+ */
89
+ public function start() {
90
+ // Execute steps
91
+ $this->run();
92
+
93
+ // Save option, progress
94
+ $this->saveOptions();
95
+
96
+ return ( object ) $this->response;
97
+ }
98
+
99
+ /**
100
+ * Execute the Current Step
101
+ * Returns false when over threshold limits are hit or when the job is done, true otherwise
102
+ * @return bool
103
+ */
104
+ protected function execute() {
105
+ // Fatal error. Let this happen never and break here immediately
106
+ if( $this->isRoot() ) {
107
+ return false;
108
+ }
109
+
110
+ // Over limits threshold
111
+ if( $this->isOverThreshold() ) {
112
+ // Prepare response and save current progress
113
+ $this->prepareResponse( false, false );
114
+ $this->saveOptions();
115
+ return false;
116
+ }
117
+
118
+ // No more steps, finished
119
+ if( $this->isFinished() ) {
120
+ $this->prepareResponse( true, false );
121
+ return false;
122
+ }
123
+
124
+ // Execute step
125
+ $stepMethodName = "step" . $this->options->currentStep;
126
+ if( !$this->{$stepMethodName}() ) {
127
+ $this->prepareResponse( false, false );
128
+ return false;
129
+ }
130
+
131
+ // Prepare Response
132
+ $this->prepareResponse();
133
+
134
+ // Not finished
135
+ return true;
136
+ }
137
+
138
+ /**
139
+ * Checks Whether There is Any Job to Execute or Not
140
+ * @return bool
141
+ */
142
+ protected function isFinished() {
143
+ return (
144
+ !isset( $this->options->isRunning ) ||
145
+ $this->options->currentStep > $this->options->totalSteps ||
146
+ !method_exists( $this, "step" . $this->options->currentStep )
147
+ );
148
+ }
149
+
150
+ /**
151
+ * Check if current operation is done on the root folder or on the live DB
152
+ * @return boolean
153
+ */
154
+ protected function isRoot() {
155
+
156
+ // Prefix is the same as the one of live site
157
+ // $wpdb = WPStaging::getInstance()->get( "wpdb" );
158
+ // if( $wpdb->prefix === $this->prefix ) {
159
+ // return true;
160
+ // }
161
+ // CloneName is empty
162
+ $name = ( array ) $this->options->cloneDirectoryName;
163
+ if( empty( $name ) ) {
164
+ return true;
165
+ }
166
+
167
+ // Live domain === Staging domain
168
+ if( $this->multisiteHomeDomain . $this->options->cloneDirectoryName === $this->multisiteHomeDomain ) {
169
+ return true;
170
+ }
171
+
172
+ return false;
173
+ }
174
+
175
+ /**
176
+ * Check if table exists
177
+ * @param string $table
178
+ * @return boolean
179
+ */
180
+ protected function isTable( $table ) {
181
+ if( $this->db->get_var( "SHOW TABLES LIKE '{$table}'" ) != $table ) {
182
+ $this->log( "Table {$table} does not exist", Logger::TYPE_ERROR );
183
+ return false;
184
+ }
185
+ return true;
186
+ }
187
+
188
+ /**
189
+ * Copy wp-config.php from the staging site if it is located outside of root one level up or
190
+ * copy default wp-config.php if production site uses bedrock or any other boilerplate solution that stores wp default config data elsewhere.
191
+ * @return boolean
192
+ */
193
+ protected function step0() {
194
+ $this->log( "Preparing Data Step0: Copy wp-config.php file", Logger::TYPE_INFO );
195
+
196
+ $dir = trailingslashit( dirname( ABSPATH ) );
197
+
198
+ $source = $dir . 'wp-config.php';
199
+
200
+ $destination = $this->options->destinationDir . 'wp-config.php';
201
+
202
+ // Check if there is already a valid wp-config.php in root of staging site
203
+ if( $this->isValidWpConfig( $destination ) ) {
204
+ $this->log( "Preparing Data Step0: Found wp-config.php file in folder {$destination}", Logger::TYPE_INFO );
205
+ return true;
206
+ }
207
+
208
+ // Check if there is a valid wp-config.php outside root of wp production site
209
+ if( $this->isValidWpConfig( $source ) ) {
210
+ // Copy it to staging site
211
+ if( $this->copy( $source, $destination ) ) {
212
+ $this->log( "Preparing Data Step0: Copy wp-config.php file from source {$source} to {$destination}", Logger::TYPE_INFO );
213
+ return true;
214
+ }
215
+ }
216
+
217
+ // No valid wp-config.php found so let's copy wp stagings default wp-config.php to staging site
218
+ $source = WPSTG_PLUGIN_DIR . "apps/Backend/helpers/wp-config.php";
219
+
220
+ $this->log( "Preparing Data Step0: Copy default wp-config.php file from source {$source} to {$destination}", Logger::TYPE_INFO );
221
+
222
+ if( $this->copy( $source, $destination ) ) {
223
+ // add missing db credentials to wp-config.php
224
+ if( !$this->alterWpConfig( $destination ) ) {
225
+ $this->log( "Preparing Data Step0: Can not alter db credentials in wp-config.php", Logger::TYPE_INFO );
226
+ return false;
227
+ }
228
+ }
229
+
230
+ $this->log( "Preparing Data Step0: Successful", Logger::TYPE_INFO );
231
+ return true;
232
+ }
233
+
234
+ /**
235
+ * Copy files with symlink support
236
+ * @param type $source
237
+ * @param type $destination
238
+ * @return boolean
239
+ */
240
+ protected function copy( $source, $destination ) {
241
+ // Copy symbolic link
242
+ if( is_link( $source ) ) {
243
+ $this->log( "Preparing Data: Symbolic link found...", Logger::TYPE_INFO );
244
+ if( !@copy( readlink( $source ), $destination ) ) {
245
+ $errors = error_get_last();
246
+ $this->log( "Preparing Data: Failed to copy {$source} Error: {$errors['message']} {$source} -> {$destination}", Logger::TYPE_ERROR );
247
+ return false;
248
+ }
249
+ }
250
+
251
+ // Copy file
252
+ if( !@copy( $source, $destination ) ) {
253
+ $errors = error_get_last();
254
+ $this->log( "Preparing Data Step0: Failed to copy {$source}! Error: {$errors['message']} {$source} -> {$destination}", Logger::TYPE_ERROR );
255
+ return false;
256
+ }
257
+
258
+ return true;
259
+ }
260
+
261
+ /**
262
+ * Make sure wp-config.php contains correct db credentials
263
+ * @param type $source
264
+ * @return boolean
265
+ */
266
+ protected function alterWpConfig( $source ) {
267
+ $this->log( "Preparing Data: Alter wp-config.php", Logger::TYPE_INFO );
268
+
269
+ $content = file_get_contents( $source );
270
+
271
+ if( false === ($content = file_get_contents( $source )) ) {
272
+ return false;
273
+ }
274
+
275
+ $search = "// ** MySQL settings ** //";
276
+
277
+ $replace = "// ** MySQL settings ** //\r\n
278
+ define( 'DB_NAME', '" . DB_NAME . "' );\r\n
279
+ /** MySQL database username */\r\n
280
+ define( 'DB_USER', '" . DB_USER . "' );\r\n
281
+ /** MySQL database password */\r\n
282
+ define( 'DB_PASSWORD', '" . DB_PASSWORD . "' );\r\n
283
+ /** MySQL hostname */\r\n
284
+ define( 'DB_HOST', '" . DB_HOST . "' );\r\n
285
+ /** Database Charset to use in creating database tables. */\r\n
286
+ define( 'DB_CHARSET', '" . DB_CHARSET . "' );\r\n
287
+ /** The Database Collate type. Don't change this if in doubt. */\r\n
288
+ define( 'DB_COLLATE', '" . DB_COLLATE . "' );\r\n";
289
+
290
+ $content = str_replace( $search, $replace, $content );
291
+
292
+ if( false === @wpstg_put_contents( $source, $content ) ) {
293
+ $this->log( "Preparing Data: Can't save wp-config.php", Logger::TYPE_ERROR );
294
+ return false;
295
+ }
296
+
297
+ return true;
298
+ }
299
+
300
+ /**
301
+ * Check if wp-config.php contains important constants
302
+ * @param type $source
303
+ * @return boolean
304
+ */
305
+ protected function isValidWpConfig( $source ) {
306
+
307
+ if( !is_file( $source ) && !is_link( $source ) ) {
308
+ $this->log( "Preparing Data Step0: Can not find {$source}", Logger::TYPE_INFO );
309
+ return false;
310
+ }
311
+
312
+ $content = file_get_contents( $source );
313
+
314
+ if( false === ($content = file_get_contents( $source )) ) {
315
+ $this->log( "Preparing Data Step0: Can not read {$source}", Logger::TYPE_INFO );
316
+ return false;
317
+ }
318
+
319
+ // Get DB_NAME from wp-config.php
320
+ preg_match( "/define\s*\(\s*['\"]DB_NAME['\"]\s*,\s*(.*)\s*\);/", $content, $matches );
321
+
322
+ if( empty( $matches[1] ) ) {
323
+ $this->log( "Preparing Data Step0: Can not find DB_NAME in wp-config.php", Logger::TYPE_INFO );
324
+ return false;
325
+ }
326
+
327
+ // Get DB_USER from wp-config.php
328
+ preg_match( "/define\s*\(\s*['\"]DB_USER['\"]\s*,\s*(.*)\s*\);/", $content, $matches );
329
+
330
+ if( empty( $matches[1] ) ) {
331
+ $this->log( "Preparing Data Step0: Can not find DB_USER in wp-config.php", Logger::TYPE_INFO );
332
+ return false;
333
+ }
334
+
335
+ // Get DB_PASSWORD from wp-config.php
336
+ preg_match( "/define\s*\(\s*['\"]DB_PASSWORD['\"]\s*,\s*(.*)\s*\);/", $content, $matches );
337
+
338
+ if( empty( $matches[1] ) ) {
339
+ $this->log( "Preparing Data Step0: Can not find DB_PASSWORD in wp-config.php", Logger::TYPE_INFO );
340
+ return false;
341
+ }
342
+
343
+ // Get DB_HOST from wp-config.php
344
+ preg_match( "/define\s*\(\s*['\"]DB_HOST['\"]\s*,\s*(.*)\s*\);/", $content, $matches );
345
+
346
+ if( empty( $matches[1] ) ) {
347
+ $this->log( "Preparing Data Step0: Can not find DB_HOST in wp-config.php", Logger::TYPE_INFO );
348
+ return false;
349
+ }
350
+ return true;
351
+ }
352
+
353
+ /**
354
+ * Replace "siteurl" and "home"
355
+ * @return bool
356
+ */
357
+ protected function step1() {
358
+ $this->log( "Preparing Data Step1: Updating siteurl and homeurl in {$this->prefix}options {$this->db->last_error}", Logger::TYPE_INFO );
359
+
360
+ // Skip - Table does not exist
361
+ if( false === $this->isTable( $this->prefix . 'options' ) ) {
362
+ return true;
363
+ }
364
+ // Skip - Table is not selected or updated
365
+ if( !in_array( $this->prefix . 'options', $this->tables ) ) {
366
+ $this->log( "Preparing Data Step1: Skipping" );
367
+ return true;
368
+ }
369
+
370
+ $this->log( "Preparing Data Step1: Updating siteurl and homeurl to " . $this->getStagingSiteUrl() );
371
+ // Replace URLs
372
+ $result = $this->db->query(
373
+ $this->db->prepare(
374
+ "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'siteurl' or option_name='home'", $this->getStagingSiteUrl()
375
+ )
376
+ );
377
+
378
+
379
+
380
+ // All good
381
+ if( $result ) {
382
+ return true;
383
+ }
384
+
385
+ $this->log( "Preparing Data Step1: Skip updating siteurl and homeurl in {$this->prefix}options. Probably already did! {$this->db->last_error}", Logger::TYPE_WARNING );
386
+ return true;
387
+ }
388
+
389
+ /**
390
+ * Update "wpstg_is_staging_site"
391
+ * @return bool
392
+ */
393
+ protected function step2() {
394
+
395
+ $this->log( "Preparing Data Step2: Updating row wpstg_is_staging_site in {$this->prefix}options {$this->db->last_error}" );
396
+
397
+ // Skip - Table does not exist
398
+ if( false === $this->isTable( $this->prefix . 'options' ) ) {
399
+ $this->log( "Preparing Data Step2: Skipping" );
400
+ return true;
401
+ }
402
+ // Skip - Table is not selected or updated
403
+ if( !in_array( $this->prefix . 'options', $this->tables ) ) {
404
+ $this->log( "Preparing Data Step2: Skipping" );
405
+ return true;
406
+ }
407
+
408
+ $result = $this->db->query(
409
+ $this->db->prepare(
410
+ "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'wpstg_is_staging_site'", "true"
411
+ )
412
+ );
413
+
414
+ // No errors but no option name such as wpstg_is_staging_site
415
+ if( '' === $this->db->last_error && 0 == $result ) {
416
+ $result = $this->db->query(
417
+ $this->db->prepare(
418
+ "INSERT INTO {$this->prefix}options (option_name,option_value) VALUES ('wpstg_is_staging_site',%s)", "true"
419
+ )
420
+ );
421
+ }
422
+
423
+ // All good
424
+ if( $result ) {
425
+ return true;
426
+ }
427
+
428
+ $this->log( "Preparing Data Step2: Failed to update wpstg_is_staging_site in {$this->prefix}options {$this->db->last_error}", Logger::TYPE_ERROR );
429
+ return false;
430
+ }
431
+
432
+ /**
433
+ * Update rewrite_rules
434
+ * @return bool
435
+ */
436
+ protected function step3() {
437
+
438
+ $this->log( "Preparing Data Step3: Updating rewrite_rules in {$this->prefix}options {$this->db->last_error}" );
439
+
440
+ // Keep Permalinks
441
+ if( isset( $this->settings->keepPermalinks ) && $this->settings->keepPermalinks === "1" ) {
442
+ $this->log( "Preparing Data Step3: Skipping" );
443
+ return true;
444
+ }
445
+
446
+ // Skip - Table does not exist
447
+ if( false === $this->isTable( $this->prefix . 'options' ) ) {
448
+ return true;
449
+ }
450
+
451
+ // Skip - Table is not selected or updated
452
+ if( !in_array( $this->prefix . 'options', $this->tables ) ) {
453
+ $this->log( "Preparing Data Step3: Skipping" );
454
+ return true;
455
+ }
456
+
457
+ $result = $this->db->query(
458
+ $this->db->prepare(
459
+ "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'rewrite_rules'", ' '
460
+ )
461
+ );
462
+
463
+ // All good
464
+ if( $result ) {
465
+ return true;
466
+ }
467
+
468
+ //$this->log( "Preparing Data Step3: Failed to update rewrite_rules in {$this->prefix}options {$this->db->last_error}", Logger::TYPE_ERROR );
469
+ return true;
470
+ }
471
+
472
+ /**
473
+ * Update Table Prefix in wp_usermeta
474
+ * @return bool
475
+ */
476
+ protected function step4() {
477
+ $this->log( "Preparing Data Step4: Updating db prefix in {$this->prefix}usermeta. " );
478
+
479
+ // Skip - Table does not exist
480
+ if( false === $this->isTable( $this->prefix . 'usermeta' ) ) {
481
+ return true;
482
+ }
483
+
484
+ // Skip - Table is not selected or updated
485
+ if( !in_array( $this->prefix . 'usermeta', $this->tables ) ) {
486
+ $this->log( "Preparing Data Step4: Skipping" );
487
+ return true;
488
+ }
489
+
490
+ // Change the base table prefix of the main network site
491
+ $this->debugLog( "Preparing Data Step4: UPDATE {$this->prefix}usermeta SET meta_key = replace(meta_key, {$this->productionDb->base_prefix}, {$this->prefix}) WHERE meta_key LIKE {$this->productionDb->base_prefix}_%" );
492
+ $update = $this->db->query(
493
+ $this->db->prepare(
494
+ "UPDATE {$this->prefix}usermeta SET meta_key = replace(meta_key, %s, %s) WHERE meta_key LIKE %s", $this->productionDb->base_prefix, $this->prefix, $this->productionDb->base_prefix . "_%"
495
+ )
496
+ );
497
+
498
+ if( false === $update ) {
499
+ $this->log( "Preparing Data Step4: Skip updating {$this->prefix}usermeta meta_key database base_prefix; {$this->db->last_error}", Logger::TYPE_INFO );
500
+ }
501
+
502
+ // Change the table prefix of the child network site
503
+ $this->debugLog( "Preparing Data Step4: UPDATE {$this->prefix}usermeta SET meta_key = replace(meta_key, {$this->db->prefix}, {$this->prefix}) WHERE meta_key LIKE {$this->db->prefix}_%" );
504
+ $update = $this->db->query(
505
+ $this->db->prepare(
506
+ "UPDATE {$this->prefix}usermeta SET meta_key = replace(meta_key, %s, %s) WHERE meta_key LIKE %s", $this->db->prefix, $this->prefix, $this->db->prefix . "_%"
507
+ )
508
+ );
509
+
510
+ if( false === $update ) {
511
+ $this->log( "Preparing Data Step4: Failed to update {$this->prefix}usermeta meta_key database table prefixes; {$this->db->last_error}", Logger::TYPE_ERROR );
512
+ $this->returnException( "Data Crunching Step 4: Failed to update {$this->prefix}usermeta meta_key database table prefixes; {$this->db->last_error}" );
513
+ return false;
514
+ }
515
+ return true;
516
+ }
517
+
518
+ /**
519
+ * Update $table_prefix in wp-config.php
520
+ * @return bool
521
+ */
522
+ protected function step5() {
523
+ $path = $this->options->destinationDir . "wp-config.php";
524
+
525
+ $this->log( "Preparing Data Step5: Updating table_prefix in {$path} to " . $this->prefix );
526
+ if( false === ($content = file_get_contents( $path )) ) {
527
+ $this->log( "Preparing Data Step5: Failed to update table_prefix in {$path}. Can't read contents", Logger::TYPE_ERROR );
528
+ return false;
529
+ }
530
+
531
+ // Replace table prefix
532
+ $pattern = '/\$table_prefix\s*=\s*(.*).*/';
533
+ $replacement = '$table_prefix = \'' . $this->prefix . '\'; // Changed by WP Staging';
534
+ $content = preg_replace( $pattern, $replacement, $content );
535
+
536
+ if( null === $content ) {
537
+ $this->log( "Preparing Data Step5: Failed to update table_prefix in {$path}. Can't read contents", Logger::TYPE_ERROR );
538
+ return false;
539
+ }
540
+
541
+ // Replace URLs
542
+ $content = str_replace( $this->multisiteHomeDomain, $this->getStagingSiteUrl(), $content );
543
+
544
+ if( false === @wpstg_put_contents( $path, $content ) ) {
545
+ $this->log( "Preparing Data Step5: Failed to update $table_prefix in {$path} to " . $this->prefix . ". Can't save contents", Logger::TYPE_ERROR );
546
+ return false;
547
+ }
548
+
549
+ return true;
550
+ }
551
+
552
+ /**
553
+ * Reset index.php to original file
554
+ * This is needed if live site is located in subfolder
555
+ * Check first if main wordpress is used in subfolder and index.php in parent directory
556
+ * @see: https://codex.wordpress.org/Giving_WordPress_Its_Own_Directory
557
+ * @return bool
558
+ */
559
+ protected function step6() {
560
+
561
+ if( !$this->isSubDir() ) {
562
+ $this->debugLog( "Preparing Data Step6: WP installation is not in a subdirectory! All good, skipping this step" );
563
+ return true;
564
+ }
565
+
566
+ $path = $this->options->destinationDir . "index.php";
567
+
568
+ if( false === ($content = file_get_contents( $path )) ) {
569
+ $this->log( "Preparing Data Step6: Failed to reset {$path} for sub directory; can't read contents", Logger::TYPE_ERROR );
570
+ return false;
571
+ }
572
+
573
+
574
+ if( !preg_match( "/(require(.*)wp-blog-header.php' \);)/", $content, $matches ) ) {
575
+ $this->log(
576
+ "Preparing Data Step6: Failed to reset index.php for sub directory; wp-blog-header.php is missing", Logger::TYPE_ERROR
577
+ );
578
+ return false;
579
+ }
580
+ $this->log( "Preparing Data: WP installation is in a subdirectory. Progressing..." );
581
+
582
+ $pattern = "/require(.*) dirname(.*) __FILE__ (.*) \. '(.*)wp-blog-header.php'(.*);.*/";
583
+
584
+ $replace = "require( dirname( __FILE__ ) . '/wp-blog-header.php' ); // " . $matches[0];
585
+ //$replace.= " // Changed by WP-Staging";
586
+
587
+
588
+
589
+ if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
590
+ $this->log( "Preparing Data: Failed to reset index.php for sub directory; replacement failed", Logger::TYPE_ERROR );
591
+ return false;
592
+ }
593
+
594
+ if( false === @wpstg_put_contents( $path, $content ) ) {
595
+ $this->log( "Preparing Data: Failed to reset index.php for sub directory; can't save contents", Logger::TYPE_ERROR );
596
+ return false;
597
+ }
598
+ $this->Log( "Preparing Data Step6: Finished successfully" );
599
+ return true;
600
+ }
601
+
602
+ /**
603
+ * Update wpstg_rmpermalinks_executed
604
+ * @return bool
605
+ */
606
+ protected function step7() {
607
+
608
+ $this->log( "Preparing Data Step7: Updating wpstg_rmpermalinks_executed in {$this->prefix}options {$this->db->last_error}" );
609
+
610
+ // Skip - Table does not exist
611
+ if( false === $this->isTable( $this->prefix . 'options' ) ) {
612
+ return true;
613
+ }
614
+
615
+ // Skip - Table is not selected or updated
616
+ if( !in_array( $this->prefix . 'options', $this->tables ) ) {
617
+ $this->log( "Preparing Data Step7: Skipping" );
618
+ return true;
619
+ }
620
+
621
+ $result = $this->db->query(
622
+ $this->db->prepare(
623
+ "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'wpstg_rmpermalinks_executed'", ' '
624
+ )
625
+ );
626
+
627
+ $this->Log( "Preparing Data Step7: Finished successfully" );
628
+ return true;
629
+ }
630
+
631
+ /**
632
+ * Update permalink_structure
633
+ * @return bool
634
+ */
635
+ protected function step8() {
636
+
637
+ $this->log( "Preparing Data Step8: Updating permalink_structure in {$this->prefix}options {$this->db->last_error}" );
638
+
639
+ // Keep Permalinks
640
+ if( isset( $this->settings->keepPermalinks ) && $this->settings->keepPermalinks === "1" ) {
641
+ $this->log( "Preparing Data Step8: Skipping" );
642
+ return true;
643
+ }
644
+
645
+ // Skip - Table does not exist
646
+ if( false === $this->isTable( $this->prefix . 'options' ) ) {
647
+ return true;
648
+ }
649
+
650
+ // Skip - Table is not selected or updated
651
+ if( !in_array( $this->prefix . 'options', $this->tables ) ) {
652
+ $this->log( "Preparing Data Step8: Skipping" );
653
+ return true;
654
+ }
655
+
656
+ $result = $this->db->query(
657
+ $this->db->prepare(
658
+ "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'permalink_structure'", ' '
659
+ )
660
+ );
661
+
662
+ // All good
663
+ if( $result ) {
664
+ $this->Log( "Preparing Data Step8: Finished successfully" );
665
+ return true;
666
+ }
667
+
668
+ $this->log( "Failed to update permalink_structure in {$this->prefix}options {$this->db->last_error}", Logger::TYPE_ERROR );
669
+ return true;
670
+ }
671
+
672
+ /**
673
+ * Update blog_public option to not allow staging site to be indexed by search engines
674
+ * @return bool
675
+ */
676
+ protected function step9() {
677
+
678
+ $this->log( "Preparing Data Step9: Set staging site to noindex" );
679
+
680
+ if( false === $this->isTable( $this->prefix . 'options' ) ) {
681
+ return true;
682
+ }
683
+
684
+ // Skip - Table is not selected or updated
685
+ if( !in_array( $this->prefix . 'options', $this->tables ) ) {
686
+ $this->log( "Preparing Data Step9: Skipping" );
687
+ return true;
688
+ }
689
+
690
+ $result = $this->db->query(
691
+ $this->db->prepare(
692
+ "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'blog_public'", '0'
693
+ )
694
+ );
695
+
696
+ // All good
697
+ if( $result ) {
698
+ $this->Log( "Preparing Data Step9: Finished successfully" );
699
+ return true;
700
+ }
701
+
702
+ $this->log( "Can not update staging site to noindex. Possible already done!", Logger::TYPE_WARNING );
703
+ return true;
704
+ }
705
+
706
+ /**
707
+ * Update WP_HOME in wp-config.php
708
+ * @return bool
709
+ */
710
+ protected function step10() {
711
+ $path = $this->options->destinationDir . "wp-config.php";
712
+
713
+ $this->log( "Preparing Data Step10: Updating WP_HOME in wp-config.php to " . $this->getStagingSiteUrl() );
714
+
715
+ if( false === ($content = file_get_contents( $path )) ) {
716
+ $this->log( "Preparing Data Step10: Failed to update WP_HOME in wp-config.php. Can't read wp-config.php", Logger::TYPE_ERROR );
717
+ return false;
718
+ }
719
+
720
+
721
+ // Get WP_HOME from wp-config.php
722
+ preg_match( "/define\s*\(\s*['\"]WP_HOME['\"]\s*,\s*(.*)\s*\);/", $content, $matches );
723
+
724
+ if( !empty( $matches[1] ) ) {
725
+ $matches[1];
726
+
727
+ $pattern = "/define\s*\(\s*['\"]WP_HOME['\"]\s*,\s*(.*)\s*\);.*/";
728
+
729
+ $replace = "define('WP_HOME','" . $this->getStagingSiteUrl() . "'); // " . $matches[1];
730
+ //$replace .= " // Changed by WP-Staging";
731
+
732
+ if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
733
+ $this->log( "Preparing Data: Failed to update WP_HOME", Logger::TYPE_ERROR );
734
+ return false;
735
+ }
736
+ } else {
737
+ $this->log( "Preparing Data Step10: WP_HOME not defined in wp-config.php. Skipping this step." );
738
+ }
739
+
740
+ if( false === @wpstg_put_contents( $path, $content ) ) {
741
+ $this->log( "Preparing Data Step10: Failed to update WP_HOME. Can't save contents", Logger::TYPE_ERROR );
742
+ return false;
743
+ }
744
+ $this->Log( "Preparing Data Step 10: Finished successfully" );
745
+ return true;
746
+ }
747
+
748
+ /**
749
+ * Update WP_SITEURL in wp-config.php
750
+ * @return bool
751
+ */
752
+ protected function step11() {
753
+ $path = $this->options->destinationDir . "wp-config.php";
754
+
755
+ $this->log( "Preparing Data Step11: Updating WP_SITEURL in wp-config.php to " . $this->getStagingSiteUrl() );
756
+
757
+ if( false === ($content = file_get_contents( $path )) ) {
758
+ $this->log( "Preparing Data Step11: Failed to update WP_SITEURL in wp-config.php. Can't read wp-config.php", Logger::TYPE_ERROR );
759
+ return false;
760
+ }
761
+
762
+
763
+ // Get WP_SITEURL from wp-config.php
764
+ preg_match( "/define\s*\(\s*['\"]WP_SITEURL['\"]\s*,\s*(.*)\s*\);/", $content, $matches );
765
+
766
+ if( !empty( $matches[1] ) ) {
767
+ $matches[1];
768
+
769
+ $pattern = "/define\s*\(\s*['\"]WP_SITEURL['\"]\s*,\s*(.*)\s*\);.*/";
770
+
771
+ $replace = "define('WP_SITEURL','" . $this->getStagingSiteUrl() . "'); // " . $matches[1];
772
+ //$replace .= " // Changed by WP-Staging";
773
+
774
+ if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
775
+ $this->log( "Preparing Data Step11: Failed to update WP_SITEURL", Logger::TYPE_ERROR );
776
+ return false;
777
+ }
778
+ } else {
779
+ $this->log( "Preparing Data Step11: WP_SITEURL not defined in wp-config.php. Skipping this step." );
780
+ }
781
+
782
+
783
+ if( false === @wpstg_put_contents( $path, $content ) ) {
784
+ $this->log( "Preparing Data Step11: Failed to update WP_SITEURL. Can't save contents", Logger::TYPE_ERROR );
785
+ return false;
786
+ }
787
+ $this->Log( "Preparing Data Step 11: Finished successfully" );
788
+ return true;
789
+ }
790
+
791
+ /**
792
+ * Update WP_ALLOW_MULTISITE constant in wp-config.php
793
+ * @return bool
794
+ */
795
+ protected function step12() {
796
+ $path = $this->options->destinationDir . "wp-config.php";
797
+
798
+ $this->log( "Preparing Data Step12: Updating WP_ALLOW_MULTISITE in wp-config.php to false" );
799
+
800
+ if( false === ($content = file_get_contents( $path )) ) {
801
+ $this->log( "Preparing Data Step12: Failed to update WP_ALLOW_MULTISITE in wp-config.php. Can't read wp-config.php", Logger::TYPE_ERROR );
802
+ return false;
803
+ }
804
+
805
+
806
+ // Get WP_SITEURL from wp-config.php
807
+ preg_match( "/define\s*\(\s*['\"]WP_ALLOW_MULTISITE['\"]\s*,\s*(.*)\s*\);/", $content, $matches );
808
+
809
+ if( !empty( $matches[1] ) ) {
810
+ $matches[1];
811
+
812
+ $pattern = "/define\s*\(\s*['\"]WP_ALLOW_MULTISITE['\"]\s*,\s*(.*)\s*\);.*/";
813
+
814
+ $replace = "define('WP_ALLOW_MULTISITE',false); // " . $matches[1];
815
+ //$replace .= " // Changed by WP-Staging";
816
+
817
+ if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
818
+ $this->log( "Preparing Data Step12: Failed to update WP_ALLOW_MULTISITE", Logger::TYPE_ERROR );
819
+ return false;
820
+ }
821
+ } else {
822
+ $this->log( "Preparing Data Step12: WP_ALLOW_MULTISITE not defined in wp-config.php. Skipping this step." );
823
+ }
824
+
825
+
826
+ if( false === @wpstg_put_contents( $path, $content ) ) {
827
+ $this->log( "Preparing Data Step12: Failed to update WP_ALLOW_MULTISITE. Can't save contents", Logger::TYPE_ERROR );
828
+ return false;
829
+ }
830
+ $this->Log( "Preparing Data: Finished Step 12 successfully" );
831
+ return true;
832
+ }
833
+
834
+ /**
835
+ * Update MULTISITE constant in wp-config.php
836
+ * @return bool
837
+ */
838
+ protected function step13() {
839
+ $path = $this->options->destinationDir . "wp-config.php";
840
+
841
+ $this->log( "Preparing Data Step13: Updating MULTISITE in wp-config.php to false" );
842
+
843
+ if( false === ($content = file_get_contents( $path )) ) {
844
+ $this->log( "Preparing Data Step13: Failed to update MULTISITE in wp-config.php. Can't read wp-config.php", Logger::TYPE_ERROR );
845
+ return false;
846
+ }
847
+
848
+
849
+ // Get WP_SITEURL from wp-config.php
850
+ preg_match( "/define\s*\(\s*['\"]MULTISITE['\"]\s*,\s*(.*)\s*\);/", $content, $matches );
851
+
852
+ if( !empty( $matches[1] ) ) {
853
+ $matches[1];
854
+
855
+ $pattern = "/define\s*\(\s*['\"]MULTISITE['\"]\s*,\s*(.*)\s*\);.*/";
856
+
857
+ $replace = "define('MULTISITE',false); // " . $matches[1];
858
+ //$replace .= " // Changed by WP-Staging";
859
+
860
+ if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
861
+ $this->log( "Preparing Data Step13: Failed to update MULTISITE", Logger::TYPE_ERROR );
862
+ return false;
863
+ }
864
+ } else {
865
+ $this->log( "Preparing Data Step13: MULTISITE not defined in wp-config.php. Skipping this step." );
866
+ }
867
+
868
+
869
+ if( false === @wpstg_put_contents( $path, $content ) ) {
870
+ $this->log( "Preparing Data Step13: Failed to update MULTISITE. Can't save contents", Logger::TYPE_ERROR );
871
+ return false;
872
+ }
873
+ $this->Log( "Preparing Data Step13: Finished successfully" );
874
+ return true;
875
+ }
876
+
877
+ /**
878
+ * Get active_sitewide_plugins from wp_sitemeta and active_plugins from subsite
879
+ * Merge both arrays and copy them to the staging site into active_plugins
880
+ */
881
+ protected function step14() {
882
+
883
+
884
+ $this->log( "Data Crunching Step14: Updating active_plugins" );
885
+
886
+ if( false === $this->isTable( $this->prefix . 'options' ) ) {
887
+ $this->log( 'Data Crunching Step14: Fatal Error ' . $this->prefix . 'options does not exist' );
888
+ $this->returnException( 'Data Crunching Step14: Fatal Error ' . $this->prefix . 'options does not exist' );
889
+ return false;
890
+ }
891
+
892
+ // Skip - Table is not selected or updated
893
+ if( !in_array( $this->prefix . 'options', $this->tables ) ) {
894
+ $this->log( "Preparing Data Step14: Skipping" );
895
+ return true;
896
+ }
897
+
898
+ // Get active_plugins value from sub site options table
899
+ $active_plugins = $this->productionDb->get_var( "SELECT option_value FROM {$this->productionDb->prefix}options WHERE option_name = 'active_plugins' " );
900
+
901
+ if( !$active_plugins ) {
902
+ $this->log( "Data Crunching Step14: Option active_plugins are empty " );
903
+ $active_plugins = array();
904
+ }
905
+ // Get active_sitewide_plugins value from main multisite wp_sitemeta table
906
+ $active_sitewide_plugins = $this->productionDb->get_var( "SELECT meta_value FROM {$this->productionDb->base_prefix}sitemeta WHERE meta_key = 'active_sitewide_plugins' " );
907
+
908
+ if( !$active_sitewide_plugins ) {
909
+ $this->log( "Data Crunching Step14: Options {$this->productionDb->base_prefix}active_sitewide_plugins is empty " );
910
+ $active_sitewide_plugins = array();
911
+ }
912
+
913
+ $active_sitewide_plugins = unserialize( $active_sitewide_plugins );
914
+ $active_plugins = unserialize( $active_plugins );
915
+
916
+ $all_plugins = array_merge( $active_plugins, array_keys( $active_sitewide_plugins ) );
917
+
918
+ sort( $all_plugins );
919
+
920
+
921
+ // Update active_plugins
922
+ $update = $this->db->query(
923
+ "UPDATE {$this->prefix}options SET option_value = '" . serialize( $all_plugins ) . "' WHERE option_name = 'active_plugins'"
924
+ );
925
+
926
+ if( false === $update ) {
927
+ $this->log( "Data Crunching Step14: Can not update option active_plugins in {$this->prefix}options", Logger::TYPE_WARNING );
928
+ return false;
929
+ }
930
+
931
+ $this->log( "Data Crunching Step14: Successful!" );
932
+ return true;
933
+ }
934
+
935
+ /**
936
+ * Update Table Prefix in wp_options
937
+ * @return bool
938
+ */
939
+ protected function step15() {
940
+ $this->log( "Preparing Data Step15: Updating db prefix in {$this->prefix}options." );
941
+
942
+ // Skip - Table does not exist
943
+ if( false === $this->isTable( $this->prefix . 'options' ) ) {
944
+ return true;
945
+ }
946
+
947
+ // Skip - Table is not selected or updated
948
+ if( !in_array( $this->prefix . 'options', $this->tables ) ) {
949
+ $this->log( "Preparing Data Step4: Skipping" );
950
+ return true;
951
+ }
952
+
953
+
954
+ $this->log( "Updating db option_names in {$this->prefix}options. " );
955
+
956
+ // Filter the rows below. Do not update them!
957
+ $filters = array(
958
+ 'wp_mail_smtp',
959
+ 'wp_mail_smtp_version',
960
+ 'wp_mail_smtp_debug',
961
+ );
962
+
963
+ $filters = apply_filters( 'wpstg_data_excl_rows', $filters );
964
+
965
+ $where = "";
966
+ foreach ( $filters as $filter ) {
967
+ $where .= " AND option_name <> '" . $filter . "'";
968
+ }
969
+
970
+ $updateOptions = $this->db->query(
971
+ $this->db->prepare(
972
+ "UPDATE IGNORE {$this->prefix}options SET option_name= replace(option_name, %s, %s) WHERE option_name LIKE %s" . $where, $this->productionDb->prefix, $this->prefix, $this->productionDb->prefix . "_%"
973
+ )
974
+ );
975
+
976
+ if( false === $updateOptions ) {
977
+ $this->log( "Preparing Data Step15: Failed to update db option_names in {$this->prefix}options. Error: {$this->db->last_error}", Logger::TYPE_WARNING );
978
+ //$this->returnException( "Data Crunching Step 15: Failed to update db option_names in {$this->prefix}options. Error: {$this->db->last_error}" );
979
+ return true;
980
+ }
981
+
982
+
983
+ return true;
984
+ }
985
+
986
+ /**
987
+ * Change upload_path in wp_options (if it is defined)
988
+ * @return bool
989
+ */
990
+ protected function step16() {
991
+ $this->log( "Preparing Data Step16: Updating upload_path {$this->prefix}options." );
992
+
993
+ // Skip - Table does not exist
994
+ if( false === $this->isTable( $this->prefix . 'options' ) ) {
995
+ return true;
996
+ }
997
+
998
+ $newUploadPath = $this->getNewUploadPath();
999
+
1000
+ if( false === $newUploadPath ) {
1001
+ $this->log( "Preparing Data Step16: Skipping" );
1002
+ return true;
1003
+ }
1004
+
1005
+ // Skip - Table is not selected or updated
1006
+ if( !in_array( $this->prefix . 'options', $this->tables ) ) {
1007
+ $this->log( "Preparing Data Step16: Skipping" );
1008
+ return true;
1009
+ }
1010
+
1011
+ $error = isset( $this->db->last_error ) ? 'Last error: ' . $this->db->last_error : '';
1012
+
1013
+ $this->log( "Updating upload_path in {$this->prefix}options. {$error}" );
1014
+
1015
+ $updateOptions = $this->db->query(
1016
+ $this->db->prepare(
1017
+ "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'upload_path'", $newUploadPath
1018
+ )
1019
+ );
1020
+
1021
+ if( false === $updateOptions ) {
1022
+ $this->log( "Preparing Data Step16: Failed to update upload_path in {$this->prefix}options. {$error}", Logger::TYPE_ERROR );
1023
+ return true;
1024
+ }
1025
+ $this->Log( "Preparing Data: Finished Step 16 successfully" );
1026
+ return true;
1027
+ }
1028
+
1029
+ /**
1030
+ * Update WP_CACHE in wp-config.php
1031
+ * @return bool
1032
+ */
1033
+ protected function step17() {
1034
+ $path = $this->options->destinationDir . "wp-config.php";
1035
+
1036
+ $this->log( "Preparing Data Step17: Set WP_CACHE in wp-config.php to false" );
1037
+
1038
+ if( false === ($content = file_get_contents( $path )) ) {
1039
+ $this->log( "Preparing Data Step17: Failed to update WP_CACHE in wp-config.php. Can't read wp-config.php", Logger::TYPE_ERROR );
1040
+ return false;
1041
+ }
1042
+
1043
+
1044
+ // Get WP_CACHE from wp-config.php
1045
+ preg_match( "/define\s*\(\s*['\"]WP_CACHE['\"]\s*,\s*(.*)\s*\);/", $content, $matches );
1046
+
1047
+ if( !empty( $matches[1] ) ) {
1048
+ $matches[1];
1049
+
1050
+ $pattern = "/define\s*\(\s*['\"]WP_CACHE['\"]\s*,\s*(.*)\s*\);.*/";
1051
+
1052
+ $replace = "define('WP_CACHE',false); // " . $matches[1];
1053
+ //$replace.= " // Changed by WP-Staging";
1054
+
1055
+ if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
1056
+ $this->log( "Preparing Data: Failed to change WP_CACHE", Logger::TYPE_ERROR );
1057
+ return false;
1058
+ }
1059
+ } else {
1060
+ $this->log( "Preparing Data Step17: WP_CACHE not defined in wp-config.php. Skipping this step." );
1061
+ }
1062
+
1063
+ if( false === @wpstg_put_contents( $path, $content ) ) {
1064
+ $this->log( "Preparing Data Step17: Failed to update WP_CACHE. Can't save contents", Logger::TYPE_ERROR );
1065
+ return false;
1066
+ }
1067
+ $this->Log( "Preparing Data: Finished Step 17 successfully" );
1068
+ return true;
1069
+ }
1070
+
1071
+ /**
1072
+ * Add UPLOADS constant in wp-config.php or change it to correct destination (multisite type /sites/2/)
1073
+ * @return bool
1074
+ */
1075
+ protected function step18() {
1076
+ $path = $this->options->destinationDir . "wp-config.php";
1077
+ $this->log( "Preparing Data Step18: Update UPLOADS constant in wp-config.php" );
1078
+ if( false === ($content = file_get_contents( $path )) ) {
1079
+ $this->log( "Preparing Data Step18: Failed to get UPLOADS in wp-config.php. Can't read wp-config.php", Logger::TYPE_ERROR );
1080
+ return false;
1081
+ }
1082
+ // Get UPLOADS from wp-config.php if there is already one
1083
+ preg_match( "/define\s*\(\s*['\"]UPLOADS['\"]\s*,\s*(.*)\s*\);/", $content, $matches );
1084
+ $uploadFolder = $this->getMultisiteUploadFolder();
1085
+ if( !empty( $matches[0] ) ) {
1086
+ $pattern = "/define\s*\(\s*'UPLOADS'\s*,\s*(.*)\s*\);/";
1087
+
1088
+ $replace = "define('UPLOADS', '" . $uploadFolder . "');";
1089
+ $this->log( "Preparing Data Step18: Change UPLOADS constant in wp-config.php to {$uploadFolder}." );
1090
+ if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
1091
+ $this->log( "Preparing Data Step 18: Failed to change UPLOADS", Logger::TYPE_ERROR );
1092
+ return false;
1093
+ }
1094
+ } else {
1095
+ $this->log( "Preparing Data Step18: UPLOADS not defined in wp-config.php. Creating new entry." );
1096
+ // Find ABSPATH and add UPLOAD constant above
1097
+ preg_match( "/if\s*\(\s*\s*!\s*defined\s*\(\s*['\"]ABSPATH['\"]\s*(.*)\s*\)\s*\)/", $content, $matches );
1098
+ if( !empty( $matches[0] ) ) {
1099
+ $matches[0];
1100
+ $pattern = "/if\s*\(\s*\s*!\s*defined\s*\(\s*['\"]ABSPATH['\"]\s*(.*)\s*\)\s*\)/";
1101
+ $replace = "define('UPLOADS', '" . $uploadFolder . "'); \n" .
1102
+ "if ( ! defined( 'ABSPATH' ) )";
1103
+ $this->log( "Preparing Data Step18: Change UPLOADS constant in wp-config.php to {$uploadFolder}." );
1104
+ if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
1105
+ $this->log( "Preparing Data Step 18: Failed to change UPLOADS", Logger::TYPE_ERROR );
1106
+ return false;
1107
+ }
1108
+ } else {
1109
+ $this->log( "Preparing Data Step 18: Can not add UPLOAD constant to wp-config.php. Can not find free position to add it.", Logger::TYPE_ERROR );
1110
+ }
1111
+ }
1112
+ if( false === @wpstg_put_contents( $path, $content ) ) {
1113
+ $this->log( "Preparing Data Step18: Failed to update UPLOADS. Can't save contents", Logger::TYPE_ERROR );
1114
+ return false;
1115
+ }
1116
+ $this->Log( "Preparing Data Step18: Finished successfully" );
1117
+ return true;
1118
+ }
1119
+
1120
+ /**
1121
+ * Update database credentials in wp-config.php
1122
+ * @return bool
1123
+ */
1124
+ protected function step19() {
1125
+ $path = $this->options->destinationDir . "wp-config.php";
1126
+
1127
+ $this->log( "Preparing Data Step19: Change database credentials in wp-config.php" );
1128
+
1129
+ if( false === ($content = file_get_contents( $path )) ) {
1130
+ $this->log( "Preparing Data Step19: Failed to update database credentials in wp-config.php. Can't read wp-config.php", Logger::TYPE_ERROR );
1131
+ return false;
1132
+ }
1133
+
1134
+
1135
+ // Get DB_NAME from wp-config.php
1136
+ preg_match( "/define\s*\(\s*['\"]DB_NAME['\"]\s*,\s*(.*)\s*\);/", $content, $matches );
1137
+
1138
+ if( !empty( $matches[1] ) ) {
1139
+ $matches[1];
1140
+
1141
+ $pattern = "/define\s*\(\s*'DB_NAME'\s*,\s*(.*)\s*\);.*/";
1142
+
1143
+ $replace = "define('DB_NAME','{$this->options->databaseDatabase}'); // " . $matches[1];
1144
+ //$replace.= " // Changed by WP-Staging";
1145
+
1146
+ if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
1147
+ $this->log( "Preparing Data: Failed to change DB_NAME", Logger::TYPE_ERROR );
1148
+ return false;
1149
+ }
1150
+ } else {
1151
+ $this->log( "Preparing Data Step19: DB_NAME not defined in wp-config.php. Skipping this step." );
1152
+ }
1153
+ // Get DB_USER from wp-config.php
1154
+ preg_match( "/define\s*\(\s*['\"]DB_USER['\"]\s*,\s*(.*)\s*\);/", $content, $matches );
1155
+
1156
+ if( !empty( $matches[1] ) ) {
1157
+ $matches[1];
1158
+
1159
+ $pattern = "/define\s*\(\s*['\"]DB_USER['\"]\s*,\s*(.*)\s*\);.*/";
1160
+
1161
+ $replace = "define('DB_USER','{$this->options->databaseUser}'); // " . $matches[1];
1162
+ //$replace.= " // Changed by WP-Staging";
1163
+
1164
+ if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
1165
+ $this->log( "Preparing Data: Failed to change DB_USER", Logger::TYPE_ERROR );
1166
+ return false;
1167
+ }
1168
+ } else {
1169
+ $this->log( "Preparing Data Step19: DB_USER not defined in wp-config.php. Skipping this step." );
1170
+ }
1171
+ // Get DB_PASSWORD from wp-config.php
1172
+ preg_match( "/define\s*\(\s*['\"]DB_PASSWORD['\"]\s*,\s*(.*)\s*\);/", $content, $matches );
1173
+
1174
+ if( !empty( $matches[1] ) ) {
1175
+ $matches[1];
1176
+
1177
+ $pattern = "/define\s*\(\s*['\"]DB_PASSWORD['\"]\s*,\s*(.*)\s*\);.*/";
1178
+
1179
+ $replace = "define('DB_PASSWORD','{$this->options->databasePassword}'); // Changed by WP Staging";
1180
+ //$replace.= " // Changed by WP-Staging";
1181
+
1182
+ if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
1183
+ $this->log( "Preparing Data: Failed to change DB_PASSWORD", Logger::TYPE_ERROR );
1184
+ return false;
1185
+ }
1186
+ } else {
1187
+ $this->log( "Preparing Data Step19: DB_PASSWORD not defined in wp-config.php. Skipping this step." );
1188
+ }
1189
+ // Get DB_HOST from wp-config.php
1190
+ preg_match( "/define\s*\(\s*['\"]DB_HOST['\"]\s*,\s*(.*)\s*\);/", $content, $matches );
1191
+
1192
+ if( !empty( $matches[1] ) ) {
1193
+ $matches[1];
1194
+
1195
+ $pattern = "/define\s*\(\s*['\"]DB_HOST['\"]\s*,\s*(.*)\s*\);.*/";
1196
+
1197
+ $replace = "define('DB_HOST','{$this->options->databaseServer}'); // " . $matches[1];
1198
+ //$replace.= " // Changed by WP-Staging";
1199
+
1200
+ if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
1201
+ $this->log( "Preparing Data: Failed to change DB_HOST", Logger::TYPE_ERROR );
1202
+ return false;
1203
+ }
1204
+ } else {
1205
+ $this->log( "Preparing Data Step19: DB_HOST not defined in wp-config.php. Skipping this step." );
1206
+ }
1207
+
1208
+
1209
+ if( false === @wpstg_put_contents( $path, $content ) ) {
1210
+ $this->log( "Preparing Data Step19: Failed to update database credentials in wp-config.php. Can't save contents", Logger::TYPE_ERROR );
1211
+ return false;
1212
+ }
1213
+ $this->Log( "Preparing Data Step 19: Finished successfully" );
1214
+ return true;
1215
+ }
1216
+
1217
+ /**
1218
+ * Save hostname of parent production site in option_name wpstg_connection
1219
+ * @return boolean
1220
+ */
1221
+ protected function step20() {
1222
+
1223
+ $table = $this->prefix . 'options';
1224
+
1225
+ $siteurl = get_site_url();
1226
+
1227
+ $connection = json_encode( array('prodHostname' => $siteurl) );
1228
+
1229
+ $data = array(
1230
+ 'option_name' => 'wpstg_connection',
1231
+ 'option_value' => $connection
1232
+ );
1233
+
1234
+ $format = array('%s', '%s');
1235
+
1236
+ $result = $this->db->replace( $table, $data, $format );
1237
+
1238
+ if( false === $result ) {
1239
+ $this->Log( "Preparing Data Step20: Could not save {$siteurl} in {$table}", Logger::TYPE_ERROR );
1240
+ }
1241
+ return true;
1242
+ }
1243
+
1244
+ /**
1245
+ * Add option_name wpstg_execute and set it to true
1246
+ * This option is used to determine if the staging website has not been loaded initiall for executing certain custom actions from \WPStaging\initActions()
1247
+ * @return boolean
1248
+ */
1249
+ protected function step21() {
1250
+
1251
+ $table = $this->prefix . 'options';
1252
+
1253
+ // Skip - Table does not exist
1254
+ if( false === $this->isTable( $table ) ) {
1255
+ return true;
1256
+ }
1257
+
1258
+
1259
+ $result = $this->db->query(
1260
+ $this->db->prepare(
1261
+ "INSERT INTO {$this->prefix}options (option_name,option_value) VALUES ('wpstg_execute',%s) ON DUPLICATE KEY UPDATE option_value = %s", "true", "true"
1262
+ )
1263
+ );
1264
+
1265
+ if( false === $result ) {
1266
+ $this->Log( "Preparing Data Step21: Could not save wpstg_execute in {$table}", Logger::TYPE_ERROR );
1267
+ }
1268
+ return true;
1269
+ }
1270
+
1271
+ /**
1272
+ * Preserve data and prevents data in wp_options from beeing cloned to staging site
1273
+ * @return bool
1274
+ */
1275
+ protected function step22() {
1276
+ $this->log( "Preparing Data Step22: Preserve Data in " . $this->prefix . "options" );
1277
+
1278
+ // Skip - Table does not exist
1279
+ if( false === $this->isTable( $this->prefix . 'options' ) ) {
1280
+ return true;
1281
+ }
1282
+
1283
+ // Skip - Table is not selected or updated
1284
+ if( !in_array( $this->prefix . 'options', $this->tables ) ) {
1285
+ $this->log( "Preparing Data Step22: Skipped" );
1286
+ return true;
1287
+ }
1288
+
1289
+ $sql = '';
1290
+
1291
+ $preserved_option_names = array('wpstg_existing_clones_beta');
1292
+
1293
+ $preserved_option_names = apply_filters( 'wpstg_preserved_options_cloning', $preserved_option_names );
1294
+ $preserved_options_escaped = esc_sql( $preserved_option_names );
1295
+
1296
+ $preserved_options_data = array();
1297
+
1298
+ // Get preserved data in wp_options tables
1299
+ $table = $this->db->prefix . 'options';
1300
+ $preserved_options_data[$this->prefix . 'options'] = $this->db->get_results(
1301
+ sprintf(
1302
+ "SELECT * FROM `{$table}` WHERE `option_name` IN ('%s')", implode( "','", $preserved_options_escaped )
1303
+ ), ARRAY_A
1304
+ );
1305
+
1306
+ // Create preserved data queries for options tables
1307
+ foreach ( $preserved_options_data as $key => $value ) {
1308
+ if( false === empty( $value ) ) {
1309
+ foreach ( $value as $option ) {
1310
+ $sql .= $this->db->prepare(
1311
+ "DELETE FROM `{$key}` WHERE `option_name` = %s;\n", $option['option_name']
1312
+ );
1313
+
1314
+ $sql .= $this->db->prepare(
1315
+ "INSERT INTO `{$key}` ( `option_id`, `option_name`, `option_value`, `autoload` ) VALUES ( NULL , %s, %s, %s );\n", $option['option_name'], $option['option_value'], $option['autoload']
1316
+ );
1317
+ }
1318
+ }
1319
+ }
1320
+
1321
+ $this->debugLog( "Preparing Data Step22: Preserve values " . json_encode( $preserved_options_data ), Logger::TYPE_INFO );
1322
+
1323
+ $this->executeSql( $sql );
1324
+
1325
+ $this->log( "Preparing Data Step22: Successful!" );
1326
+ return true;
1327
+ }
1328
+
1329
+ /**
1330
+ * Execute a batch of sql queries
1331
+ * @param string $sqlbatch
1332
+ */
1333
+ private function executeSql( $sqlbatch ) {
1334
+ $queries = array_filter( explode( ";\n", $sqlbatch ) );
1335
+
1336
+ foreach ( $queries as $query ) {
1337
+ if( false === $this->db->query( $query ) ) {
1338
+ $this->log( "Data Crunching Warning: Can not execute query {$query}", Logger::TYPE_WARNING );
1339
+ }
1340
+ }
1341
+ return true;
1342
+ }
1343
+
1344
+ /**
1345
+ * Get relative path to the uploads media folder of multisite e.g.
1346
+ * wp-content/uploads/sites/SITEID or old wordpress structure wp-content/blogs.dir/SITEID/files
1347
+ * @return boolean
1348
+ */
1349
+ protected function getMultisiteUploadFolder() {
1350
+ $strings = new Strings();
1351
+ // Get absolute path to uploads folder
1352
+ $uploads = wp_upload_dir();
1353
+ $basedir = $uploads['basedir'];
1354
+ // Get relative upload path
1355
+ $relDir = str_replace( wpstg_replace_windows_directory_separator( ABSPATH ), null, wpstg_replace_windows_directory_separator( $basedir ) );
1356
+ return $relDir;
1357
+ }
1358
+
1359
+ /**
1360
+ * Get Upload Path to staging site
1361
+ * @return boolean|string
1362
+ */
1363
+ protected function getNewUploadPath() {
1364
+ $uploadPath = get_option( 'upload_path' );
1365
+
1366
+ if( !$uploadPath ) {
1367
+ return false;
1368
+ }
1369
+
1370
+ $customSlug = str_replace( wpstg_replace_windows_directory_separator( \WPStaging\WPStaging::getWPpath() ), '', wpstg_replace_windows_directory_separator( $uploadPath ) );
1371
+
1372
+ $newUploadPath = wpstg_replace_windows_directory_separator( \WPStaging\WPStaging::getWPpath() ) . $this->options->cloneDirectoryName . '/' . $customSlug;
1373
+
1374
+ return $newUploadPath;
1375
+ }
1376
+
1377
+ /**
1378
+ * Return URL to staging site
1379
+ * @return string
1380
+ */
1381
+ protected function getStagingSiteUrl() {
1382
+
1383
+ if( !empty( $this->options->cloneHostname ) ) {
1384
+ return $this->options->cloneHostname;
1385
+ }
1386
+
1387
+ if( $this->isSubDir() ) {
1388
+ return trailingslashit( $this->multisiteHomeDomain ) . trailingslashit( $this->getSubDir() ) . $this->options->cloneDirectoryName;
1389
+ }
1390
+
1391
+ // Get the path to the main multisite without appending and trailingslash e.g. wordpress
1392
+ $multisitePath = defined( 'PATH_CURRENT_SITE' ) ? PATH_CURRENT_SITE : '/';
1393
+ $url = rtrim( $this->multisiteHomeDomain, '/\\' ) . $multisitePath . $this->options->cloneDirectoryName;
1394
+
1395
+ return $url;
1396
+ }
1397
+
1398
+ /**
1399
+ * Check if WP is installed in subdir
1400
+ * @return boolean
1401
+ */
1402
+ protected function isSubDir() {
1403
+ // Compare names without scheme to bypass cases where siteurl and home have different schemes http / https
1404
+ // This is happening much more often than you would expect
1405
+ $siteurl = preg_replace( '#^https?://#', '', rtrim( get_option( 'siteurl' ), '/' ) );
1406
+ $home = preg_replace( '#^https?://#', '', rtrim( get_option( 'home' ), '/' ) );
1407
+
1408
+ if( $home !== $siteurl ) {
1409
+ return true;
1410
+ }
1411
+ return false;
1412
+ }
1413
+
1414
+ /**
1415
+ * Get the install sub directory if WP is installed in sub directory
1416
+ * @return string
1417
+ */
1418
+ protected function getSubDir() {
1419
+ $home = get_option( 'home' );
1420
+ $siteurl = get_option( 'siteurl' );
1421
+
1422
+ if( empty( $home ) || empty( $siteurl ) ) {
1423
+ return '';
1424
+ }
1425
+
1426
+ $dir = str_replace( $home, '', $siteurl );
1427
+ return str_replace( '/', '', $dir );
1428
+ }
1429
+
1430
+ }
apps/Backend/Modules/Jobs/Multisite/Database.php CHANGED
@@ -1,366 +1,366 @@
1
- <?php
2
-
3
- namespace WPStaging\Backend\Modules\Jobs\Multisite;
4
-
5
- // No Direct Access
6
- if( !defined( "WPINC" ) ) {
7
- die;
8
- }
9
-
10
- use WPStaging\WPStaging;
11
- use WPStaging\Utils\Strings;
12
- use WPStaging\Backend\Modules\Jobs\JobExecutable;
13
-
14
- /**
15
- * Class Database
16
- * @package WPStaging\Backend\Modules\Jobs
17
- */
18
- class Database extends JobExecutable {
19
-
20
- /**
21
- * @var int
22
- */
23
- private $total = 0;
24
-
25
- /**
26
- * @var \WPDB
27
- */
28
- private $db;
29
-
30
- /**
31
- * Initialize
32
- */
33
- public function initialize() {
34
- $this->db = WPStaging::getInstance()->get( "wpdb" );
35
- $this->getTables();
36
- // Add wp_users and wp_usermeta to the tables object because they are not available in MU installation but we need them on the staging site
37
- $this->total = count( $this->options->tables );
38
- $this->isFatalError();
39
- }
40
-
41
- private function getTables() {
42
- // Add wp_users and wp_usermeta to the tables if they do not exist
43
- // because they are not available in MU installation but we need them on the staging site
44
-
45
- if( !in_array( $this->db->prefix . 'users', $this->options->tables ) ) {
46
- array_push( $this->options->tables, $this->db->prefix . 'users' );
47
- $this->saveOptions();
48
- }
49
- if( !in_array( $this->db->prefix . 'usermeta', $this->options->tables ) ) {
50
- array_push( $this->options->tables, $this->db->prefix . 'usermeta' );
51
- $this->saveOptions();
52
- }
53
- }
54
-
55
- /**
56
- * Return fatal error and stops here if subfolder already exists
57
- * and mainJob is not updating the clone
58
- * @return boolean
59
- */
60
- private function isFatalError() {
61
- //$path = trailingslashit( get_home_path() ) . $this->options->cloneDirectoryName;
62
- $path = trailingslashit($this->options->cloneDir);
63
- if( isset( $this->options->mainJob ) && $this->options->mainJob !== 'updating' && is_dir( $path ) && !wpstg_is_empty_dir( $path) ) {
64
- $this->returnException( " Can not continue! Change the name of the clone or delete existing folder. Then try again. Folder already exists and is not empty: " . $path );
65
- }
66
- return false;
67
- }
68
-
69
- /**
70
- * Calculate Total Steps in This Job and Assign It to $this->options->totalSteps
71
- * @return void
72
- */
73
- protected function calculateTotalSteps() {
74
- $this->options->totalSteps = ($this->total === 0) ? 1 : $this->total;
75
- }
76
-
77
- /**
78
- * Execute the Current Step
79
- * Returns false when over threshold limits are hit or when the job is done, true otherwise
80
- * @return bool
81
- */
82
- protected function execute() {
83
-
84
-
85
- // Over limits threshold
86
- if( $this->isOverThreshold() ) {
87
- // Prepare response and save current progress
88
- $this->prepareResponse( false, false );
89
- $this->saveOptions();
90
- return false;
91
- }
92
-
93
- // No more steps, finished
94
- if( !isset( $this->options->isRunning ) || $this->options->currentStep > $this->total ) {
95
- $this->prepareResponse( true, false );
96
- return false;
97
- }
98
-
99
- // Copy table
100
- if( isset( $this->options->tables[$this->options->currentStep] ) && !$this->copyTable( $this->options->tables[$this->options->currentStep] ) ) {
101
- // Prepare Response
102
- $this->prepareResponse( false, false );
103
-
104
- // Not finished
105
- return true;
106
- }
107
-
108
- // if( isset( $this->options->tables[$this->options->currentStep] ) && $this->db->prefix . 'users' === $this->options->tables[$this->options->currentStep] ) {
109
- // $this->copyWpUsers();
110
- // }
111
- // if( isset( $this->options->tables[$this->options->currentStep] ) && $this->db->prefix . 'usermeta' === $this->options->tables[$this->options->currentStep] ) {
112
- // $this->copyWpUsermeta();
113
- // }
114
- // Prepare Response
115
- $this->prepareResponse();
116
-
117
- // Not finished
118
- return true;
119
- }
120
-
121
- /**
122
- * Get new prefix for the staging site
123
- * @return string
124
- */
125
- private function getStagingPrefix() {
126
- $stagingPrefix = $this->options->prefix;
127
- // Make sure prefix of staging site is NEVER identical to prefix of live site!
128
- if( $stagingPrefix == $this->db->prefix ) {
129
- $error = 'Fatal error 7: The new database table prefix ' . $stagingPrefix . ' would be identical to the table prefix of the live site. Please open a support ticket at support@wp-staging.com';
130
- $this->returnException($error);
131
- wp_die( $error );
132
-
133
- }
134
- return $stagingPrefix;
135
- }
136
-
137
- /**
138
- * No worries, SQL queries don't eat from PHP execution time!
139
- * @param string $tableName
140
- * @return bool
141
- */
142
- private function copyTable( $tableName ) {
143
-
144
- $strings = new Strings();
145
- $tableName = is_object( $tableName ) ? $tableName->name : $tableName;
146
- $newTableName = $this->getStagingPrefix() . $strings->str_replace_first( $this->db->prefix, null, $tableName );
147
-
148
- // Get wp_users from main site
149
- if( 'users' === $strings->str_replace_first( $this->db->prefix, null, $tableName ) ) {
150
- $tableName = $this->db->base_prefix . 'users';
151
- }
152
- // Get wp_usermeta from main site
153
- if( 'usermeta' === $strings->str_replace_first( $this->db->prefix, null, $tableName ) ) {
154
- $tableName = $this->db->base_prefix . 'usermeta';
155
- }
156
-
157
- // Drop table if necessary
158
- $this->dropTable( $newTableName );
159
-
160
- // Save current job
161
- $this->setJob( $newTableName );
162
-
163
- // Beginning of the job
164
- if( !$this->startJob( $newTableName, $tableName ) ) {
165
- return true;
166
- }
167
-
168
- // Copy data
169
- $this->copyData( $newTableName, $tableName );
170
-
171
- // Finish the step
172
- return $this->finishStep();
173
- }
174
-
175
- /**
176
- * Copy multisite global user table wp_users to wpstgX_users
177
- * @return bool
178
- */
179
- // private function copyWpUsers() {
180
- //// $strings = new Strings();
181
- //// $tableName = $this->db->base_prefix . 'users';
182
- //// $newTableName = $this->getStagingPrefix() . $strings->str_replace_first( $this->db->base_prefix, null, $tableName );
183
- //
184
- // $tableName = $this->db->base_prefix . 'users';
185
- // $newTableName = $this->getStagingPrefix() . 'users';
186
- //
187
- // $this->log( "DB Copy: Try to create table {$newTableName}" );
188
- //
189
- // // Drop table if necessary
190
- // $this->dropTable( $newTableName );
191
- //
192
- // // Save current job
193
- // $this->setJob( $newTableName );
194
- //
195
- // // Beginning of the job
196
- // if( !$this->startJob( $newTableName, $tableName ) ) {
197
- // return true;
198
- // }
199
- //
200
- // // Copy data
201
- // $this->copyData( $newTableName, $tableName );
202
- //
203
- // // Finish the step
204
- // return $this->finishStep();
205
- // }
206
-
207
- /**
208
- * Copy multisite global user table wp_usermeta to wpstgX_users
209
- * @return bool
210
- */
211
- // private function copyWpUsermeta() {
212
- //// $strings = new Strings();
213
- //// $tableName = $this->db->base_prefix . 'usermeta';
214
- //// $newTableName = $this->getStagingPrefix() . $strings->str_replace_first( $this->db->base_prefix, null, $tableName );
215
- // $tableName = $this->db->base_prefix . 'usermeta';
216
- // $newTableName = $this->getStagingPrefix() . 'usermeta';
217
- //
218
- // $this->log( "DB Copy: Try to create table {$newTableName}" );
219
- //
220
- //
221
- // // Drop table if necessary
222
- // $this->dropTable( $newTableName );
223
- //
224
- // // Save current job
225
- // $this->setJob( $newTableName );
226
- //
227
- // // Beginning of the job
228
- // if( !$this->startJob( $newTableName, $tableName ) ) {
229
- // return true;
230
- // }
231
- // // Copy data
232
- // $this->copyData( $newTableName, $tableName );
233
- //
234
- // // Finish the step
235
- // return $this->finishStep();
236
- // }
237
-
238
- /**
239
- * Copy data from old table to new table
240
- * @param string $new
241
- * @param string $old
242
- */
243
- private function copyData( $new, $old ) {
244
- $rows = $this->options->job->start + $this->settings->queryLimit;
245
- $this->log(
246
- "DB Copy: {$old} as {$new} from {$this->options->job->start} to {$rows} records"
247
- );
248
-
249
- $limitation = '';
250
-
251
- if( 0 < ( int ) $this->settings->queryLimit ) {
252
- $limitation = " LIMIT {$this->settings->queryLimit} OFFSET {$this->options->job->start}";
253
- }
254
-
255
- $this->db->query(
256
- "INSERT INTO {$new} SELECT * FROM {$old} {$limitation}"
257
- );
258
-
259
- // Set new offset
260
- $this->options->job->start += $this->settings->queryLimit;
261
- }
262
-
263
- /**
264
- * Set the job
265
- * @param string $table
266
- */
267
- private function setJob( $table ) {
268
- if( isset( $this->options->job->current ) ) {
269
- return;
270
- }
271
-
272
- $this->options->job->current = $table;
273
- $this->options->job->start = 0;
274
- }
275
-
276
- /**
277
- * Start Job
278
- * @param string $new
279
- * @param string $old
280
- * @return bool
281
- */
282
- private function startJob( $new, $old ) {
283
-
284
- if( 0 != $this->options->job->start ) {
285
- return true;
286
- }
287
-
288
- // Table does not exist
289
- $result = $this->db->query( "SHOW TABLES LIKE '{$old}'" );
290
- if( !$result || 0 === $result ) {
291
- return true;
292
- }
293
-
294
- $this->log( "DB Copy: Creating table {$new}" );
295
-
296
- $this->db->query( "CREATE TABLE {$new} LIKE {$old}" );
297
-
298
- $this->options->job->total = 0;
299
- $this->options->job->total = ( int ) $this->db->get_var( "SELECT COUNT(1) FROM {$old}" );
300
-
301
- if( 0 == $this->options->job->total ) {
302
- $this->finishStep();
303
- return false;
304
- }
305
-
306
- return true;
307
- }
308
-
309
- /**
310
- * Finish the step
311
- */
312
- private function finishStep() {
313
- // This job is not finished yet
314
- if( $this->options->job->total > $this->options->job->start ) {
315
- return false;
316
- }
317
-
318
- // Add it to cloned tables listing
319
- $this->options->clonedTables[] = isset( $this->options->tables[$this->options->currentStep] ) ? $this->options->tables[$this->options->currentStep] : false;
320
-
321
- // Reset job
322
- $this->options->job = new \stdClass();
323
-
324
- return true;
325
- }
326
-
327
- /**
328
- * Drop table if necessary
329
- * @param string $new
330
- */
331
- private function dropTable( $new ) {
332
-
333
- $old = $this->db->get_var( $this->db->prepare( "SHOW TABLES LIKE %s", $new ) );
334
-
335
- if( !$this->shouldDropTable( $new, $old ) ) {
336
- return;
337
- }
338
-
339
- $this->log( "DB Copy: {$new} already exists, dropping it first" );
340
- $this->db->query( "SET FOREIGN_KEY_CHECKS=0" );
341
- $this->db->query( "DROP TABLE {$new}" );
342
- $this->db->query( "SET FOREIGN_KEY_CHECKS=1" );
343
- }
344
-
345
- /**
346
- * Check if table needs to be dropped
347
- * @param string $new
348
- * @param string $old
349
- * @return bool
350
- */
351
- private function shouldDropTable( $new, $old ) {
352
-
353
-
354
-
355
- if( $old === $new &&
356
- (
357
- !isset( $this->options->job->current ) ||
358
- !isset( $this->options->job->start ) ||
359
- 0 == $this->options->job->start
360
- ) ) {
361
- return true;
362
- }
363
- return false;
364
- }
365
-
366
- }
1
+ <?php
2
+
3
+ namespace WPStaging\Backend\Modules\Jobs\Multisite;
4
+
5
+ // No Direct Access
6
+ if( !defined( "WPINC" ) ) {
7
+ die;
8
+ }
9
+
10
+ use WPStaging\WPStaging;
11
+ use WPStaging\Utils\Strings;
12
+ use WPStaging\Backend\Modules\Jobs\JobExecutable;
13
+
14
+ /**
15
+ * Class Database
16
+ * @package WPStaging\Backend\Modules\Jobs
17
+ */
18
+ class Database extends JobExecutable {
19
+
20
+ /**
21
+ * @var int
22
+ */
23
+ private $total = 0;
24
+
25
+ /**
26
+ * @var \WPDB
27
+ */
28
+ private $db;
29
+
30
+ /**
31
+ * Initialize
32
+ */
33
+ public function initialize() {
34
+ $this->db = WPStaging::getInstance()->get( "wpdb" );
35
+ $this->getTables();
36
+ // Add wp_users and wp_usermeta to the tables object because they are not available in MU installation but we need them on the staging site
37
+ $this->total = count( $this->options->tables );
38
+ $this->isFatalError();
39
+ }
40
+
41
+ private function getTables() {
42
+ // Add wp_users and wp_usermeta to the tables if they do not exist
43
+ // because they are not available in MU installation but we need them on the staging site
44
+
45
+ if( !in_array( $this->db->prefix . 'users', $this->options->tables ) ) {
46
+ array_push( $this->options->tables, $this->db->prefix . 'users' );
47
+ $this->saveOptions();
48
+ }
49
+ if( !in_array( $this->db->prefix . 'usermeta', $this->options->tables ) ) {
50
+ array_push( $this->options->tables, $this->db->prefix . 'usermeta' );
51
+ $this->saveOptions();
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Return fatal error and stops here if subfolder already exists
57
+ * and mainJob is not updating the clone
58
+ * @return boolean
59
+ */
60
+ private function isFatalError() {
61
+ //$path = trailingslashit( get_home_path() ) . $this->options->cloneDirectoryName;
62
+ $path = trailingslashit($this->options->cloneDir);
63
+ if( isset( $this->options->mainJob ) && $this->options->mainJob !== 'updating' && is_dir( $path ) && !wpstg_is_empty_dir( $path) ) {
64
+ $this->returnException( " Can not continue! Change the name of the clone or delete existing folder. Then try again. Folder already exists and is not empty: " . $path );
65
+ }
66
+ return false;
67
+ }
68
+
69
+ /**
70
+ * Calculate Total Steps in This Job and Assign It to $this->options->totalSteps
71
+ * @return void
72
+ */
73
+ protected function calculateTotalSteps() {
74
+ $this->options->totalSteps = ($this->total === 0) ? 1 : $this->total;
75
+ }
76
+
77
+ /**
78
+ * Execute the Current Step
79
+ * Returns false when over threshold limits are hit or when the job is done, true otherwise
80
+ * @return bool
81
+ */
82
+ protected function execute() {
83
+
84
+
85
+ // Over limits threshold
86
+ if( $this->isOverThreshold() ) {
87
+ // Prepare response and save current progress
88
+ $this->prepareResponse( false, false );
89
+ $this->saveOptions();
90
+ return false;
91
+ }
92
+
93
+ // No more steps, finished
94
+ if( !isset( $this->options->isRunning ) || $this->options->currentStep > $this->total ) {
95
+ $this->prepareResponse( true, false );
96
+ return false;
97
+ }
98
+
99
+ // Copy table
100
+ if( isset( $this->options->tables[$this->options->currentStep] ) && !$this->copyTable( $this->options->tables[$this->options->currentStep] ) ) {
101
+ // Prepare Response
102
+ $this->prepareResponse( false, false );
103
+
104
+ // Not finished
105
+ return true;
106
+ }
107
+
108
+ // if( isset( $this->options->tables[$this->options->currentStep] ) && $this->db->prefix . 'users' === $this->options->tables[$this->options->currentStep] ) {
109
+ // $this->copyWpUsers();
110
+ // }
111
+ // if( isset( $this->options->tables[$this->options->currentStep] ) && $this->db->prefix . 'usermeta' === $this->options->tables[$this->options->currentStep] ) {
112
+ // $this->copyWpUsermeta();
113
+ // }
114
+ // Prepare Response
115
+ $this->prepareResponse();
116
+
117
+ // Not finished
118
+ return true;
119
+ }
120
+
121
+ /**
122
+ * Get new prefix for the staging site
123
+ * @return string
124
+ */
125
+ private function getStagingPrefix() {
126
+ $stagingPrefix = $this->options->prefix;
127
+ // Make sure prefix of staging site is NEVER identical to prefix of live site!
128
+ if( $stagingPrefix == $this->db->prefix ) {
129
+ $error = 'Fatal error 7: The new database table prefix ' . $stagingPrefix . ' would be identical to the table prefix of the live site. Please open a support ticket at support@wp-staging.com';
130
+ $this->returnException($error);
131
+ wp_die( $error );
132
+
133
+ }
134
+ return $stagingPrefix;
135
+ }
136
+
137
+ /**
138
+ * No worries, SQL queries don't eat from PHP execution time!
139
+ * @param string $tableName
140
+ * @return bool
141
+ */
142
+ private function copyTable( $tableName ) {
143
+
144
+ $strings = new Strings();
145
+ $tableName = is_object( $tableName ) ? $tableName->name : $tableName;
146
+ $newTableName = $this->getStagingPrefix() . $strings->str_replace_first( $this->db->prefix, null, $tableName );
147
+
148
+ // Get wp_users from main site
149
+ if( 'users' === $strings->str_replace_first( $this->db->prefix, null, $tableName ) ) {
150
+ $tableName = $this->db->base_prefix . 'users';
151
+ }
152
+ // Get wp_usermeta from main site
153
+ if( 'usermeta' === $strings->str_replace_first( $this->db->prefix, null, $tableName ) ) {
154
+ $tableName = $this->db->base_prefix . 'usermeta';
155
+ }
156
+
157
+ // Drop table if necessary
158
+ $this->dropTable( $newTableName );
159
+
160
+ // Save current job
161
+ $this->setJob( $newTableName );
162
+
163
+ // Beginning of the job
164
+ if( !$this->startJob( $newTableName, $tableName ) ) {
165
+ return true;
166
+ }
167
+
168
+ // Copy data
169
+ $this->copyData( $newTableName, $tableName );
170
+
171
+ // Finish the step
172
+ return $this->finishStep();
173
+ }
174
+
175
+ /**
176
+ * Copy multisite global user table wp_users to wpstgX_users
177
+ * @return bool
178
+ */
179
+ // private function copyWpUsers() {
180
+ //// $strings = new Strings();
181
+ //// $tableName = $this->db->base_prefix . 'users';
182
+ //// $newTableName = $this->getStagingPrefix() . $strings->str_replace_first( $this->db->base_prefix, null, $tableName );
183
+ //
184
+ // $tableName = $this->db->base_prefix . 'users';
185
+ // $newTableName = $this->getStagingPrefix() . 'users';
186
+ //
187
+ // $this->log( "DB Copy: Try to create table {$newTableName}" );
188
+ //
189
+ // // Drop table if necessary
190
+ // $this->dropTable( $newTableName );
191
+ //
192
+ // // Save current job
193
+ // $this->setJob( $newTableName );
194
+ //
195
+ // // Beginning of the job
196
+ // if( !$this->startJob( $newTableName, $tableName ) ) {
197
+ // return true;
198
+ // }
199
+ //
200
+ // // Copy data
201
+ // $this->copyData( $newTableName, $tableName );
202
+ //
203
+ // // Finish the step
204
+ // return $this->finishStep();
205
+ // }
206
+
207
+ /**
208
+ * Copy multisite global user table wp_usermeta to wpstgX_users
209
+ * @return bool
210
+ */
211
+ // private function copyWpUsermeta() {
212
+ //// $strings = new Strings();
213
+ //// $tableName = $this->db->base_prefix . 'usermeta';
214
+ //// $newTableName = $this->getStagingPrefix() . $strings->str_replace_first( $this->db->base_prefix, null, $tableName );
215
+ // $tableName = $this->db->base_prefix . 'usermeta';
216
+ // $newTableName = $this->getStagingPrefix() . 'usermeta';
217
+ //
218
+ // $this->log( "DB Copy: Try to create table {$newTableName}" );
219
+ //
220
+ //
221
+ // // Drop table if necessary
222
+ // $this->dropTable( $newTableName );
223
+ //
224
+ // // Save current job
225
+ // $this->setJob( $newTableName );
226
+ //
227
+ // // Beginning of the job
228
+ // if( !$this->startJob( $newTableName, $tableName ) ) {
229
+ // return true;
230
+ // }
231
+ // // Copy data
232
+ // $this->copyData( $newTableName, $tableName );
233
+ //
234
+ // // Finish the step
235
+ // return $this->finishStep();
236
+ // }
237
+
238
+ /**
239
+ * Copy data from old table to new table
240
+ * @param string $new
241
+ * @param string $old
242
+ */
243
+ private function copyData( $new, $old ) {
244
+ $rows = $this->options->job->start + $this->settings->queryLimit;
245
+ $this->log(
246
+ "DB Copy: {$old} as {$new} from {$this->options->job->start} to {$rows} records"
247
+ );
248
+
249
+ $limitation = '';
250
+
251
+ if( 0 < ( int ) $this->settings->queryLimit ) {
252
+ $limitation = " LIMIT {$this->settings->queryLimit} OFFSET {$this->options->job->start}";
253
+ }
254
+
255
+ $this->db->query(
256
+ "INSERT INTO {$new} SELECT * FROM {$old} {$limitation}"
257
+ );
258
+
259
+ // Set new offset
260
+ $this->options->job->start += $this->settings->queryLimit;
261
+ }
262
+
263
+ /**
264
+ * Set the job
265
+ * @param string $table
266
+ */
267
+ private function setJob( $table ) {
268
+ if( isset( $this->options->job->current ) ) {
269
+ return;
270
+ }
271
+
272
+ $this->options->job->current = $table;
273
+ $this->options->job->start = 0;
274
+ }
275
+
276
+ /**
277
+ * Start Job
278
+ * @param string $new
279
+ * @param string $old
280
+ * @return bool
281
+ */
282
+ private function startJob( $new, $old ) {
283
+
284
+ if( 0 != $this->options->job->start ) {
285
+ return true;
286
+ }
287
+
288
+ // Table does not exist
289
+ $result = $this->db->query( "SHOW TABLES LIKE '{$old}'" );
290
+ if( !$result || 0 === $result ) {
291
+ return true;
292
+ }
293
+
294
+ $this->log( "DB Copy: Creating table {$new}" );
295
+
296
+ $this->db->query( "CREATE TABLE {$new} LIKE {$old}" );
297
+
298
+ $this->options->job->total = 0;
299
+ $this->options->job->total = ( int ) $this->db->get_var( "SELECT COUNT(1) FROM {$old}" );
300
+
301
+ if( 0 == $this->options->job->total ) {
302
+ $this->finishStep();
303
+ return false;
304
+ }
305
+
306
+ return true;
307
+ }
308
+
309
+ /**
310
+ * Finish the step
311
+ */
312
+ private function finishStep() {
313
+ // This job is not finished yet
314
+ if( $this->options->job->total > $this->options->job->start ) {
315
+ return false;
316
+ }
317
+
318
+ // Add it to cloned tables listing
319
+ $this->options->clonedTables[] = isset( $this->options->tables[$this->options->currentStep] ) ? $this->options->tables[$this->options->currentStep] : false;
320
+
321
+ // Reset job
322
+ $this->options->job = new \stdClass();
323
+
324
+ return true;
325
+ }
326
+
327
+ /**
328
+ * Drop table if necessary
329
+ * @param string $new
330
+ */
331
+ private function dropTable( $new ) {
332
+
333
+ $old = $this->db->get_var( $this->db->prepare( "SHOW TABLES LIKE %s", $new ) );
334
+
335
+ if( !$this->shouldDropTable( $new, $old ) ) {
336
+ return;
337
+ }
338
+
339
+ $this->log( "DB Copy: {$new} already exists, dropping it first" );
340
+ $this->db->query( "SET FOREIGN_KEY_CHECKS=0" );
341
+ $this->db->query( "DROP TABLE {$new}" );
342
+ $this->db->query( "SET FOREIGN_KEY_CHECKS=1" );
343
+ }
344
+
345
+ /**
346
+ * Check if table needs to be dropped
347
+ * @param string $new
348
+ * @param string $old
349
+ * @return bool
350
+ */
351
+ private function shouldDropTable( $new, $old ) {
352
+
353
+
354
+
355
+ if( $old === $new &&
356
+ (
357
+ !isset( $this->options->job->current ) ||
358
+ !isset( $this->options->job->start ) ||
359
+ 0 == $this->options->job->start
360
+ ) ) {
361
+ return true;
362
+ }
363
+ return false;
364
+ }
365
+
366
+ }
apps/Backend/Modules/Jobs/Multisite/DatabaseExternal.php CHANGED
@@ -1,446 +1,446 @@
1
- <?php
2
-
3
- namespace WPStaging\Backend\Modules\Jobs\Multisite;
4
-
5
- // No Direct Access
6
- if( !defined( "WPINC" ) ) {
7
- die;
8
- }
9
-
10
- use WPStaging\WPStaging;
11
- use WPStaging\Utils\Strings;
12
- use WPStaging\Backend\Modules\Jobs\JobExecutable;
13
-
14
- /**
15
- * Class Database
16
- * @package WPStaging\Backend\Modules\Jobs
17
- */
18
- class DatabaseExternal extends JobExecutable {
19
-
20
- /**
21
- * @var int
22
- */
23
- private $total = 0;
24
-
25
- /**
26
- * @var \WPDB
27
- */
28
- private $db;
29
-
30
- /**
31
- * Staging Database
32
- * @var \WPDB
33
- */
34
- private $stagingDb;
35
-
36
- /**
37
- * Helper class for string methods
38
- * @var object
39
- */
40
- private $strings;
41
-
42
- /**
43
- * Initialize
44
- */
45
- public function initialize() {
46
- $this->db = WPStaging::getInstance()->get( "wpdb" );
47
- $this->stagingDb = $this->getExternalDBConnection();
48
- $this->getTables();
49
- // Add 2 more tables to total count because we need to copy two more tables from the main multisite installation wp_users and wp_usermeta
50
- $this->total = count( $this->options->tables ) + 2;
51
- $this->isFatalError();
52
- $this->strings = new Strings();
53
- }
54
-
55
- /**
56
- * Get external db object
57
- * @return mixed db | false
58
- */
59
- private function getExternalDBConnection() {
60
-
61
- $db = new \wpdb( $this->options->databaseUser, str_replace( "\\\\", "\\", $this->options->databasePassword ), $this->options->databaseDatabase, $this->options->databaseServer );
62
-
63
- // Can not connect to mysql
64
- if( !empty( $db->error->errors['db_connect_fail']['0'] ) ) {
65
- $this->returnException( "Can not connect to external database {$this->options->databaseDatabase}" );
66
- return false;
67
- }
68
-
69
- // Can not connect to database
70
- $db->select( $this->options->databaseDatabase );
71
- if( !$db->ready ) {
72
- $error = isset( $db->error->errors['db_select_fail'] ) ? $db->error->errors['db_select_fail'] : "Error: Can't select {database} Either it does not exist or you don't have privileges to access it.";
73
- $this->returnException( $error );
74
- exit;
75
- }
76
- return $db;
77
- }
78
-
79
- /**
80
- * Add wp_users and wp_usermeta to the tables if they do not exist
81
- * because they are not available in MU installation but we need them on the staging site
82
- *
83
- * return void
84
- */
85
- private function getTables() {
86
-
87
- if( !in_array( $this->db->prefix . 'users', $this->options->tables ) ) {
88
- array_push( $this->options->tables, $this->db->prefix . 'users' );
89
- $this->saveOptions();
90
- }
91
- if( !in_array( $this->db->prefix . 'usermeta', $this->options->tables ) ) {
92
- array_push( $this->options->tables, $this->db->prefix . 'usermeta' );
93
- $this->saveOptions();
94
- }
95
- }
96
-
97
- /**
98
- * Return fatal error and stops here if subfolder already exists & is not empty
99
- * and mainJob is not updating the clone
100
- * @return boolean
101
- */
102
- private function isFatalError() {
103
- //$path = trailingslashit( get_home_path() ) . $this->options->cloneDirectoryName;
104
- $path = trailingslashit($this->options->cloneDir);
105
- if( isset( $this->options->mainJob ) && $this->options->mainJob !== 'updating' && (is_dir( $path ) && !wpstg_is_empty_dir( $path ) ) ) {
106
- $this->returnException( " Can not continue! Change the name of the clone or delete existing folder. Then try again. Folder already exists and is not empty: " . $path );
107
- }
108
- return false;
109
- }
110
-
111
- /**
112
- * Calculate Total Steps in This Job and Assign It to $this->options->totalSteps
113
- * @return void
114
- */
115
- protected function calculateTotalSteps() {
116
- $this->options->totalSteps = ($this->total === 0) ? 1 : $this->total;
117
- }
118
-
119
- /**
120
- * Execute the Current Step
121
- * Returns false when over threshold limits are hit or when the job is done, true otherwise
122
- * @return bool
123
- */
124
- protected function execute() {
125
-
126
-
127
- // Over limits threshold
128
- if( $this->isOverThreshold() ) {
129
- // Prepare response and save current progress
130
- $this->prepareResponse( false, false );
131
- $this->saveOptions();
132
- return false;
133
- }
134
-
135
- // No more steps, finished
136
- if( !isset( $this->options->isRunning ) || $this->options->currentStep > $this->total ) {
137
- $this->prepareResponse( true, false );
138
- return false;
139
- }
140
-
141
- // Copy table
142
- if( isset( $this->options->tables[$this->options->currentStep] ) && !$this->copyTable( $this->options->tables[$this->options->currentStep] ) ) {
143
- // Prepare Response
144
- $this->prepareResponse( false, false );
145
-
146
- // Not finished
147
- return true;
148
- }
149
-
150
- // Prepare Response
151
- $this->prepareResponse();
152
-
153
- // Not finished
154
- return true;
155
- }
156
-
157
- /**
158
- * Get new prefix for the staging site
159
- * @return string
160
- */
161
- private function getStagingPrefix() {
162
-
163
- $this->options->prefix = !empty( $this->options->databasePrefix ) ? $this->options->databasePrefix : $this->db->prefix;
164
-
165
- return $this->options->prefix;
166
- }
167
-
168
- /**
169
- * No worries, SQL queries don't eat from PHP execution time!
170
- * @param string $tableName
171
- * @return bool
172
- */
173
- private function copyTable( $tableName ) {
174
-
175
- $tableName = is_object( $tableName ) ? $tableName->name : $tableName;
176
- $newTableName = $this->getStagingPrefix() . $this->strings->str_replace_first( $this->db->prefix, null, $tableName );
177
-
178
- // Get wp_users from main site
179
- if( 'users' === $this->strings->str_replace_first( $this->db->prefix, null, $tableName ) ) {
180
- $tableName = $this->db->base_prefix . 'users';
181
- }
182
- // Get wp_usermeta from main site
183
- if( 'usermeta' === $this->strings->str_replace_first( $this->db->prefix, null, $tableName ) ) {
184
- $tableName = $this->db->base_prefix . 'usermeta';
185
- }
186
-
187
- // Drop table if necessary
188
- $this->dropTable( $newTableName );
189
-
190
- // Save current job
191
- $this->setJob( $newTableName );
192
-
193
- // Beginning of the job
194
- if( !$this->startJob( $newTableName, $tableName ) ) {
195
- return true;
196
- }
197
-
198
- // Copy data
199
- $this->copyData( $newTableName, $tableName );
200
-
201
- // Finish the step
202
- return $this->finishStep();
203
- }
204
-
205
- /**
206
- * Set the job
207
- * @param string $table
208
- */
209
- private function setJob( $table ) {
210
- if( isset( $this->options->job->current ) ) {
211
- return;
212
- }
213
-
214
- $this->options->job->current = $table;
215
- $this->options->job->start = 0;
216
- }
217
-
218
- /**
219
- * Start Job and create tables
220
- * @param string $new
221
- * @param string $old
222
- * @return bool
223
- */
224
- private function startJob( $new, $old ) {
225
-
226
- if( $this->isExcludedTable( $new ) ) {
227
- return false;
228
- }
229
-
230
- if( 0 != $this->options->job->start ) {
231
- return true;
232
- }
233
-
234
- // Table does not exist
235
- $result = $this->db->query( "SHOW TABLES LIKE '{$old}'" );
236
- if( false === $result || 0 === $result ) {
237
- $this->log( "DB External Copy: Table {$this->options->databaseDatabase}.{$old} does not exists. Skip!" );
238
- return true;
239
- }
240
-
241
- $this->log( "DB External Copy: CREATE table {$this->options->databaseDatabase}.{$new}" );
242
- $sql = $this->getCreateStatement( $old );
243
-
244
- $search = '';
245
- // Get table 'wp_users' from main site
246
- if( 'users' === $this->strings->str_replace_first( $this->db->base_prefix, null, $old ) ) {
247
- $search = $this->db->prefix . 'users';
248
- }
249
- // Get table 'wp_usermeta' from main site
250
- if( 'usermeta' === $this->strings->str_replace_first( $this->db->base_prefix, null, $old ) ) {
251
- $search = $this->db->prefix . 'usermeta';
252
- }
253
-
254
- // Replace table prefix to the new one
255
- $sql = str_replace( "CREATE TABLE `{$search}`", "CREATE TABLE `{$new}`", $sql );
256
-
257
- $this->stagingDb->query('SET FOREIGN_KEY_CHECKS=0;');
258
-
259
- if( false === $this->stagingDb->query( $sql ) ) {
260
- $this->returnException( "DB External Copy - Fatal Error: {$this->stagingDb->last_error} Query: {$sql}" );
261
- }
262
-
263
-
264
- // Count amount of rows to insert with next step
265
- $this->options->job->total = 0;
266
- $this->options->job->total = ( int ) $this->db->get_var( "SELECT COUNT(1) FROM `{$this->db->dbname}`.`{$old}`" );
267
-
268
- $this->log( "DB External Copy: Table {$old} contains {$this->options->job->total} rows " );
269
-
270
-
271
- if( 0 == $this->options->job->total ) {
272
- $this->finishStep();
273
- return false;
274
- }
275
-
276
- return true;
277
- }
278
-
279
- /**
280
- * Get MySQL query create table
281
- *
282
- * @param string $table_name Table name
283
- * @return array
284
- */
285
- private function getCreateStatement( $tableName ) {
286
-
287
- // Query: Default
288
- $row = $this->db->get_results( "SHOW CREATE TABLE `{$tableName}`", ARRAY_A );
289
-
290
- // Query: Get wp_users from main site
291
- if( 'users' === $this->strings->str_replace_first( $this->db->base_prefix, null, $tableName ) ) {
292
- //$tableName = $this->db->base_prefix . 'users';
293
- //$row = $this->db->get_results( "SHOW CREATE TABLE `{$tableName}`", ARRAY_A );
294
- $row[0] = str_replace( $tableName, $this->db->prefix . 'users', $row[0] );
295
- }
296
- // Query: Get wp_usermeta from main site
297
- if( 'usermeta' === $this->strings->str_replace_first( $this->db->base_prefix, null, $tableName ) ) {
298
- //$tableName = $this->db->base_prefix . 'usermeta';
299
- //$row = $this->db->get_results( "SHOW CREATE TABLE `{$tableName}`", ARRAY_A );
300
- $row[0] = str_replace( $tableName, $this->db->prefix . 'usermeta', $row[0] );
301
- }
302
-
303
- // Get create table
304
- if( isset( $row[0]['Create Table'] ) ) {
305
- return $row[0]['Create Table'];
306
- }
307
- return array();
308
- }
309
-
310
- /**
311
- * Copy data from old table to new table
312
- * @param string $new
313
- * @param string $old
314
- */
315
- private function copyData( $new, $old ) {
316
-
317
- $rows = $this->options->job->start + $this->settings->queryLimit;
318
- $this->log(
319
- "DB External Copy: INSERT {$this->db->dbname}.{$old} as {$this->options->databaseDatabase}.{$new} from {$this->options->job->start} to {$rows} records"
320
- );
321
-
322
- $limitation = '';
323
-
324
- if( 0 < ( int ) $this->settings->queryLimit ) {
325
- $limitation = " LIMIT {$this->settings->queryLimit} OFFSET {$this->options->job->start}";
326
- }
327
-
328
- // Get data from production site
329
- $rows = $this->db->get_results( "SELECT * FROM `{$old}` {$limitation}", ARRAY_A );
330
-
331
- // Start transaction
332
- $this->stagingDb->query( 'SET autocommit=0;' );
333
- $this->stagingDb->query('SET FOREIGN_KEY_CHECKS=0;');
334
- $this->stagingDb->query( 'START TRANSACTION;' );
335
-
336
- // Copy into staging site
337
- foreach ( $rows as $row ) {
338
- $escaped_values = $this->mysqlEscapeMimic( array_values( $row ) );
339
- $values = implode( "', '", $escaped_values );
340
- $this->stagingDb->query( "INSERT INTO `{$new}` VALUES ('{$values}')" );
341
- }
342
- // Commit transaction
343
- $this->stagingDb->query( 'COMMIT;' );
344
- $this->stagingDb->query( 'SET autocommit=1;' );
345
-
346
-
347
- // Set new offset
348
- $this->options->job->start += $this->settings->queryLimit;
349
- }
350
-
351
- /**
352
- * Mimics the mysql_real_escape_string function. Adapted from a post by 'feedr' on php.net.
353
- * @link http://php.net/manual/en/function.mysql-real-escape-string.php#101248
354
- * @access public
355
- * @param string $input The string to escape.
356
- * @return string
357
- */
358
- private function mysqlEscapeMimic( $input ) {
359
- if( is_array( $input ) ) {
360
- return array_map( __METHOD__, $input );
361
- }
362
- if( !empty( $input ) && is_string( $input ) ) {
363
- return str_replace( array('\\', "\0", "\n", "\r", "'", '"', "\x1a"), array('\\\\', '\\0', '\\n', '\\r', "\\'", '\\"', '\\Z'), $input );
364
- }
365
-
366
- return $input;
367
- }
368
-
369
- /**
370
- * Is table excluded from search replace processing?
371
- * @param string $table
372
- * @return boolean
373
- */
374
- private function isExcludedTable( $table ) {
375
-
376
- $customTables = apply_filters( 'wpstg_clone_database_tables_exclude', array() );
377
- $defaultTables = array('blogs', 'blog_versions');
378
-
379
- $tables = array_merge( $customTables, $defaultTables );
380
-
381
- $excludedTables = array();
382
- foreach ( $tables as $key => $value ) {
383
- $excludedTables[] = $this->options->prefix . $value;
384
- }
385
-
386
- if( in_array( $table, $excludedTables ) ) {
387
- return true;
388
- }
389
- return false;
390
- }
391
-
392
- /**
393
- * Finish the step
394
- */
395
- private function finishStep() {
396
- // This job is not finished yet
397
- if( $this->options->job->total > $this->options->job->start ) {
398
- return false;
399
- }
400
-
401
- // Add it to cloned tables listing
402
- $this->options->clonedTables[] = isset( $this->options->tables[$this->options->currentStep] ) ? $this->options->tables[$this->options->currentStep] : false;
403
-
404
- // Reset job
405
- $this->options->job = new \stdClass();
406
-
407
- return true;
408
- }
409
-
410
- /**
411
- * Drop table if necessary
412
- * @param string $new
413
- */
414
- private function dropTable( $new ) {
415
-
416
- $old = $this->stagingDb->get_var( $this->stagingDb->prepare( "SHOW TABLES LIKE %s", $new ) );
417
-
418
- if( !$this->shouldDropTable( $new, $old ) ) {
419
- return;
420
- }
421
-
422
- $this->log( "DB External Copy: {$new} already exists, dropping it first" );
423
- $this->stagingDb->query( "SET FOREIGN_KEY_CHECKS=0" );
424
- $this->stagingDb->query( "DROP TABLE {$new}" );
425
- $this->stagingDb->query( "SET FOREIGN_KEY_CHECKS=1" );
426
- }
427
-
428
- /**
429
- * Check if table needs to be dropped
430
- * @param string $new
431
- * @param string $old
432
- * @return bool
433
- */
434
- private function shouldDropTable( $new, $old ) {
435
- if( $old === $new &&
436
- (
437
- !isset( $this->options->job->current ) ||
438
- !isset( $this->options->job->start ) ||
439
- 0 == $this->options->job->start
440
- ) ) {
441
- return true;
442
- }
443
- return false;
444
- }
445
-
446
- }
1
+ <?php
2
+
3
+ namespace WPStaging\Backend\Modules\Jobs\Multisite;
4
+
5
+ // No Direct Access
6
+ if( !defined( "WPINC" ) ) {
7
+ die;
8
+ }
9
+
10
+ use WPStaging\WPStaging;
11
+ use WPStaging\Utils\Strings;
12
+ use WPStaging\Backend\Modules\Jobs\JobExecutable;
13
+
14
+ /**
15
+ * Class Database
16
+ * @package WPStaging\Backend\Modules\Jobs
17
+ */
18
+ class DatabaseExternal extends JobExecutable {
19
+
20
+ /**
21
+ * @var int
22
+ */
23
+ private $total = 0;
24
+
25
+ /**
26
+ * @var \WPDB
27
+ */
28
+ private $db;
29
+
30
+ /**
31
+ * Staging Database
32
+ * @var \WPDB
33
+ */
34
+ private $stagingDb;
35
+
36
+ /**
37
+ * Helper class for string methods
38
+ * @var object
39
+ */
40
+ private $strings;
41
+
42
+ /**
43
+ * Initialize
44
+ */
45
+ public function initialize() {
46
+ $this->db = WPStaging::getInstance()->get( "wpdb" );
47
+ $this->stagingDb = $this->getExternalDBConnection();
48
+ $this->getTables();
49
+ // Add 2 more tables to total count because we need to copy two more tables from the main multisite installation wp_users and wp_usermeta
50
+ $this->total = count( $this->options->tables ) + 2;
51
+ $this->isFatalError();
52
+ $this->strings = new Strings();
53
+ }
54
+
55
+ /**
56
+ * Get external db object
57
+ * @return mixed db | false
58
+ */
59
+ private function getExternalDBConnection() {
60
+
61
+ $db = new \wpdb( $this->options->databaseUser, str_replace( "\\\\", "\\", $this->options->databasePassword ), $this->options->databaseDatabase, $this->options->databaseServer );
62
+
63
+ // Can not connect to mysql
64
+ if( !empty( $db->error->errors['db_connect_fail']['0'] ) ) {
65
+ $this->returnException( "Can not connect to external database {$this->options->databaseDatabase}" );
66
+ return false;
67
+ }
68
+
69
+ // Can not connect to database
70
+ $db->select( $this->options->databaseDatabase );
71
+ if( !$db->ready ) {
72
+ $error = isset( $db->error->errors['db_select_fail'] ) ? $db->error->errors['db_select_fail'] : "Error: Can't select {database} Either it does not exist or you don't have privileges to access it.";
73
+ $this->returnException( $error );
74
+ exit;
75
+ }
76
+ return $db;
77
+ }
78
+
79
+ /**
80
+ * Add wp_users and wp_usermeta to the tables if they do not exist
81
+ * because they are not available in MU installation but we need them on the staging site
82
+ *
83
+ * return void
84
+ */
85
+ private function getTables() {
86
+
87
+ if( !in_array( $this->db->prefix . 'users', $this->options->tables ) ) {
88
+ array_push( $this->options->tables, $this->db->prefix . 'users' );
89
+ $this->saveOptions();
90
+ }
91
+ if( !in_array( $this->db->prefix . 'usermeta', $this->options->tables ) ) {
92
+ array_push( $this->options->tables, $this->db->prefix . 'usermeta' );
93
+ $this->saveOptions();
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Return fatal error and stops here if subfolder already exists & is not empty
99
+ * and mainJob is not updating the clone
100
+ * @return boolean
101
+ */
102
+ private function isFatalError() {
103
+ //$path = trailingslashit( get_home_path() ) . $this->options->cloneDirectoryName;
104
+ $path = trailingslashit($this->options->cloneDir);
105
+ if( isset( $this->options->mainJob ) && $this->options->mainJob !== 'updating' && (is_dir( $path ) && !wpstg_is_empty_dir( $path ) ) ) {
106
+ $this->returnException( " Can not continue! Change the name of the clone or delete existing folder. Then try again. Folder already exists and is not empty: " . $path );
107
+ }
108
+ return false;
109
+ }
110
+
111
+ /**
112
+ * Calculate Total Steps in This Job and Assign It to $this->options->totalSteps
113
+ * @return void
114
+ */
115
+ protected function calculateTotalSteps() {
116
+ $this->options->totalSteps = ($this->total === 0) ? 1 : $this->total;
117
+ }
118
+
119
+ /**
120
+ * Execute the Current Step
121
+ * Returns false when over threshold limits are hit or when the job is done, true otherwise
122
+ * @return bool
123
+ */
124
+ protected function execute() {
125
+
126
+
127
+ // Over limits threshold
128
+ if( $this->isOverThreshold() ) {
129
+ // Prepare response and save current progress
130
+ $this->prepareResponse( false, false );
131
+ $this->saveOptions();
132
+ return false;
133
+ }
134
+
135
+ // No more steps, finished
136
+ if( !isset( $this->options->isRunning ) || $this->options->currentStep > $this->total ) {
137
+ $this->prepareResponse( true, false );
138
+ return false;
139
+ }
140
+
141
+ // Copy table
142
+ if( isset( $this->options->tables[$this->options->currentStep] ) && !$this->copyTable( $this->options->tables[$this->options->currentStep] ) ) {
143
+ // Prepare Response
144
+ $this->prepareResponse( false, false );
145
+
146
+ // Not finished
147
+ return true;
148
+ }
149
+
150
+ // Prepare Response
151
+ $this->prepareResponse();
152
+
153
+ // Not finished
154
+ return true;
155
+ }
156
+
157
+ /**
158
+ * Get new prefix for the staging site
159
+ * @return string
160
+ */
161
+ private function getStagingPrefix() {
162
+
163
+ $this->options->prefix = !empty( $this->options->databasePrefix ) ? $this->options->databasePrefix : $this->db->prefix;
164
+
165
+ return $this->options->prefix;
166
+ }
167
+
168
+ /**
169
+ * No worries, SQL queries don't eat from PHP execution time!
170
+ * @param string $tableName
171
+ * @return bool
172
+ */
173
+ private function copyTable( $tableName ) {
174
+
175
+ $tableName = is_object( $tableName ) ? $tableName->name : $tableName;
176
+ $newTableName = $this->getStagingPrefix() . $this->strings->str_replace_first( $this->db->prefix, null, $tableName );
177
+
178
+ // Get wp_users from main site
179
+ if( 'users' === $this->strings->str_replace_first( $this->db->prefix, null, $tableName ) ) {
180
+ $tableName = $this->db->base_prefix . 'users';
181
+ }
182
+ // Get wp_usermeta from main site
183
+ if( 'usermeta' === $this->strings->str_replace_first( $this->db->prefix, null, $tableName ) ) {
184
+ $tableName = $this->db->base_prefix . 'usermeta';
185
+ }
186
+
187
+ // Drop table if necessary
188
+ $this->dropTable( $newTableName );
189
+
190
+ // Save current job
191
+ $this->setJob( $newTableName );
192
+
193
+ // Beginning of the job
194
+ if( !$this->startJob( $newTableName, $tableName ) ) {
195
+ return true;
196
+ }
197
+
198
+ // Copy data
199
+ $this->copyData( $newTableName, $tableName );
200
+
201
+ // Finish the step
202
+ return $this->finishStep();
203
+ }
204
+
205
+ /**
206
+ * Set the job
207
+ * @param string $table
208
+ */
209
+ private function setJob( $table ) {
210
+ if( isset( $this->options->job->current ) ) {
211
+ return;
212
+ }
213
+
214
+ $this->options->job->current = $table;
215
+ $this->options->job->start = 0;
216
+ }
217
+
218
+ /**
219
+ * Start Job and create tables
220
+ * @param string $new
221
+ * @param string $old
222
+ * @return bool
223
+ */
224
+ private function startJob( $new, $old ) {
225
+
226
+ if( $this->isExcludedTable( $new ) ) {
227
+ return false;
228
+ }
229
+
230
+ if( 0 != $this->options->job->start ) {
231
+ return true;
232
+ }
233
+
234
+ // Table does not exist
235
+ $result = $this->db->query( "SHOW TABLES LIKE '{$old}'" );
236
+ if( false === $result || 0 === $result ) {
237
+ $this->log( "DB External Copy: Table {$this->options->databaseDatabase}.{$old} does not exists. Skip!" );
238
+ return true;
239
+ }
240
+
241
+ $this->log( "DB External Copy: CREATE table {$this->options->databaseDatabase}.{$new}" );
242
+ $sql = $this->getCreateStatement( $old );
243
+
244
+ $search = '';
245
+ // Get table 'wp_users' from main site
246
+ if( 'users' === $this->strings->str_replace_first( $this->db->base_prefix, null, $old ) ) {
247
+ $search = $this->db->prefix . 'users';
248
+ }
249
+ // Get table 'wp_usermeta' from main site
250
+ if( 'usermeta' === $this->strings->str_replace_first( $this->db->base_prefix, null, $old ) ) {
251
+ $search = $this->db->prefix . 'usermeta';
252
+ }
253
+
254
+ // Replace table prefix to the new one
255
+ $sql = str_replace( "CREATE TABLE `{$search}`", "CREATE TABLE `{$new}`", $sql );
256
+
257
+ $this->stagingDb->query('SET FOREIGN_KEY_CHECKS=0;');
258
+
259
+ if( false === $this->stagingDb->query( $sql ) ) {
260
+ $this->returnException( "DB External Copy - Fatal Error: {$this->stagingDb->last_error} Query: {$sql}" );
261
+ }
262
+
263
+
264
+ // Count amount of rows to insert with next step
265
+ $this->options->job->total = 0;
266
+ $this->options->job->total = ( int ) $this->db->get_var( "SELECT COUNT(1) FROM `{$this->db->dbname}`.`{$old}`" );
267
+
268
+ $this->log( "DB External Copy: Table {$old} contains {$this->options->job->total} rows " );
269
+
270
+
271
+ if( 0 == $this->options->job->total ) {
272
+ $this->finishStep();
273
+ return false;
274
+ }
275
+
276
+ return true;
277
+ }
278
+
279
+ /**
280
+ * Get MySQL query create table
281
+ *
282
+ * @param string $table_name Table name
283
+ * @return array
284
+ */
285
+ private function getCreateStatement( $tableName ) {
286
+
287
+ // Query: Default
288
+ $row = $this->db->get_results( "SHOW CREATE TABLE `{$tableName}`", ARRAY_A );
289
+
290
+ // Query: Get wp_users from main site
291
+ if( 'users' === $this->strings->str_replace_first( $this->db->base_prefix, null, $tableName ) ) {
292
+ //$tableName = $this->db->base_prefix . 'users';
293
+ //$row = $this->db->get_results( "SHOW CREATE TABLE `{$tableName}`", ARRAY_A );
294
+ $row[0] = str_replace( $tableName, $this->db->prefix . 'users', $row[0] );
295
+ }
296
+ // Query: Get wp_usermeta from main site
297
+ if( 'usermeta' === $this->strings->str_replace_first( $this->db->base_prefix, null, $tableName ) ) {
298
+ //$tableName = $this->db->base_prefix . 'usermeta';
299
+ //$row = $this->db->get_results( "SHOW CREATE TABLE `{$tableName}`", ARRAY_A );
300
+ $row[0] = str_replace( $tableName, $this->db->prefix . 'usermeta', $row[0] );
301
+ }
302
+
303
+ // Get create table
304
+ if( isset( $row[0]['Create Table'] ) ) {
305
+ return $row[0]['Create Table'];
306
+ }
307
+ return array();
308
+ }
309
+
310
+ /**
311
+ * Copy data from old table to new table
312
+ * @param string $new
313
+ * @param string $old
314
+ */
315
+ private function copyData( $new, $old ) {
316
+
317
+ $rows = $this->options->job->start + $this->settings->queryLimit;
318
+ $this->log(
319
+ "DB External Copy: INSERT {$this->db->dbname}.{$old} as {$this->options->databaseDatabase}.{$new} from {$this->options->job->start} to {$rows} records"
320
+ );
321
+
322
+ $limitation = '';
323
+
324
+ if( 0 < ( int ) $this->settings->queryLimit ) {
325
+ $limitation = " LIMIT {$this->settings->queryLimit} OFFSET {$this->options->job->start}";
326
+ }
327
+
328
+ // Get data from production site
329
+ $rows = $this->db->get_results( "SELECT * FROM `{$old}` {$limitation}", ARRAY_A );
330
+
331
+ // Start transaction
332
+ $this->stagingDb->query( 'SET autocommit=0;' );
333
+ $this->stagingDb->query('SET FOREIGN_KEY_CHECKS=0;');
334
+ $this->stagingDb->query( 'START TRANSACTION;' );
335
+
336
+ // Copy into staging site
337
+ foreach ( $rows as $row ) {
338
+ $escaped_values = $this->mysqlEscapeMimic( array_values( $row ) );
339
+ $values = implode( "', '", $escaped_values );
340
+ $this->stagingDb->query( "INSERT INTO `{$new}` VALUES ('{$values}')" );
341
+ }
342
+ // Commit transaction
343
+ $this->stagingDb->query( 'COMMIT;' );
344
+ $this->stagingDb->query( 'SET autocommit=1;' );
345
+
346
+
347
+ // Set new offset
348
+ $this->options->job->start += $this->settings->queryLimit;
349
+ }
350
+
351
+ /**
352
+ * Mimics the mysql_real_escape_string function. Adapted from a post by 'feedr' on php.net.
353
+ * @link http://php.net/manual/en/function.mysql-real-escape-string.php#101248
354
+ * @access public
355
+ * @param string $input The string to escape.
356
+ * @return string
357
+ */
358
+ private function mysqlEscapeMimic( $input ) {
359
+ if( is_array( $input ) ) {
360
+ return array_map( __METHOD__, $input );
361
+ }
362
+ if( !empty( $input ) && is_string( $input ) ) {
363
+ return str_replace( array('\\', "\0", "\n", "\r", "'", '"', "\x1a"), array('\\\\', '\\0', '\\n', '\\r', "\\'", '\\"', '\\Z'), $input );
364
+ }
365
+
366
+ return $input;
367
+ }
368
+
369
+ /**
370
+ * Is table excluded from search replace processing?
371
+ * @param string $table
372
+ * @return boolean
373
+ */
374
+ private function isExcludedTable( $table ) {
375
+
376
+ $customTables = apply_filters( 'wpstg_clone_database_tables_exclude', array() );
377
+ $defaultTables = array('blogs', 'blog_versions');
378
+
379
+ $tables = array_merge( $customTables, $defaultTables );
380
+
381
+ $excludedTables = array();
382
+ foreach ( $tables as $key => $value ) {
383
+ $excludedTables[] = $this->options->prefix . $value;
384
+ }
385
+
386
+ if( in_array( $table, $excludedTables ) ) {
387
+ return true;
388
+ }
389
+ return false;
390
+ }
391
+
392
+ /**
393
+ * Finish the step
394
+ */
395
+ private function finishStep() {
396
+ // This job is not finished yet
397
+ if( $this->options->job->total > $this->options->job->start ) {
398
+ return false;
399
+ }
400
+
401
+ // Add it to cloned tables listing
402
+ $this->options->clonedTables[] = isset( $this->options->tables[$this->options->currentStep] ) ? $this->options->tables[$this->options->currentStep] : false;
403
+
404
+ // Reset job
405
+ $this->options->job = new \stdClass();
406
+
407
+ return true;
408
+ }
409
+
410
+ /**
411
+ * Drop table if necessary
412
+ * @param string $new
413
+ */
414
+ private function dropTable( $new ) {
415
+
416
+ $old = $this->stagingDb->get_var( $this->stagingDb->prepare( "SHOW TABLES LIKE %s", $new ) );
417
+
418
+ if( !$this->shouldDropTable( $new, $old ) ) {
419
+ return;
420
+ }
421
+
422
+ $this->log( "DB External Copy: {$new} already exists, dropping it first" );
423
+ $this->stagingDb->query( "SET FOREIGN_KEY_CHECKS=0" );
424
+ $this->stagingDb->query( "DROP TABLE {$new}" );
425
+ $this->stagingDb->query( "SET FOREIGN_KEY_CHECKS=1" );
426
+ }
427
+
428
+ /**
429
+ * Check if table needs to be dropped
430
+ * @param string $new
431
+ * @param string $old
432
+ * @return bool
433
+ */
434
+ private function shouldDropTable( $new, $old ) {
435
+ if( $old === $new &&
436
+ (
437
+ !isset( $this->options->job->current ) ||
438
+ !isset( $this->options->job->start ) ||
439
+ 0 == $this->options->job->start
440
+ ) ) {
441
+ return true;
442
+ }
443
+ return false;
444
+ }
445
+
446
+ }
apps/Backend/Modules/Jobs/Multisite/Directories.php CHANGED
@@ -1,609 +1,609 @@
1
- <?php
2
-
3
- namespace WPStaging\Backend\Modules\Jobs\Multisite;
4
-
5
- // No Direct Access
6
- if( !defined( "WPINC" ) ) {
7
- die;
8
- }
9
-
10
- use WPStaging\WPStaging;
11
- use WPStaging\Utils\Logger;
12
- use WPStaging\Utils\Strings;
13
- use WPStaging\Iterators\RecursiveDirectoryIterator;
14
- use WPStaging\Iterators\RecursiveFilterExclude;
15
- use WPStaging\Backend\Modules\Jobs\JobExecutable;
16
-
17
- /**
18
- * Class Files
19
- * @package WPStaging\Backend\Modules\Directories
20
- */
21
- class Directories extends JobExecutable {
22
-
23
- /**
24
- * @var array
25
- */
26
- private $files = array();
27
-
28
- /**
29
- * Total steps to do
30
- * @var int
31
- */
32
- private $total = 5;
33
-
34
- /**
35
- * path to the cache file
36
- * @var string
37
- */
38
- private $filename;
39
-
40
- /**
41
- * Initialize
42
- */
43
- public function initialize() {
44
- $this->filename = $this->cache->getCacheDir() . "files_to_copy." . $this->cache->getCacheExtension();
45
- }
46
-
47
- /**
48
- * Calculate Total Steps in This Job and Assign It to $this->options->totalSteps
49
- * @return void
50
- */
51
- protected function calculateTotalSteps() {
52
-
53
- $this->options->totalSteps = $this->total + count( $this->options->extraDirectories );
54
- }
55
-
56
- /**
57
- * Start Module
58
- * @return object
59
- */
60
- public function start() {
61
-
62
- // Execute steps
63
- $this->run();
64
-
65
- // Save option, progress
66
- $this->saveProgress();
67
-
68
- return ( object ) $this->response;
69
- }
70
-
71
- /**
72
- * Step 0
73
- * Get WP Root files
74
- * Does not collect any sub folders
75
- */
76
- private function getWpRootFiles() {
77
-
78
- // Skip it
79
- if( $this->isDirectoryExcluded( \WPStaging\WPStaging::getWPpath() . 'wp-admin' . DIRECTORY_SEPARATOR ) ) {
80
- $this->log( "Skip " . \WPStaging\WPStaging::getWPpath() . 'wp-admin' . DIRECTORY_SEPARATOR );
81
- return true;
82
- }
83
-
84
- // open file handle
85
- $files = $this->open( $this->filename, 'a' );
86
-
87
- try {
88
-
89
- // Iterate over wp root directory
90
- $iterator = new \DirectoryIterator( \WPStaging\WPStaging::getWPpath() );
91
-
92
- $this->log( "Scanning / for files" );
93
-
94
- // Write path line
95
- foreach ( $iterator as $item ) {
96
- if( !$item->isDot() && $item->isFile() ) {
97
- if( $this->write( $files, $iterator->getFilename() . PHP_EOL ) ) {
98
- $this->options->totalFiles++;
99
-
100
- // Too much cpu time
101
- //$this->options->totalFileSize += $iterator->getSize();
102
- }
103
- }
104
- }
105
- } catch ( \Exception $e ) {
106
- $this->returnException( 'Error: ' . $e->getMessage() );
107
- }
108
-
109
- $this->close( $files );
110
- return true;
111
- }
112
-
113
- /**
114
- * Step 2
115
- * Get WP Content Files without multisite folder wp-content/uploads/sites or wp-content/blogs.dir/
116
- */
117
- private function getWpContentFiles() {
118
-
119
- // Skip it
120
- if( $this->isDirectoryExcluded( WP_CONTENT_DIR ) ) {
121
- $this->log( "Skip " . WP_CONTENT_DIR );
122
- return true;
123
- }
124
- // open file handle
125
- $files = $this->open( $this->filename, 'a' );
126
-
127
- /**
128
- * Excluded folders relative to the folder to iterate
129
- */
130
- $excludePaths = array(
131
- 'cache',
132
- 'plugins/wps-hide-login',
133
- 'uploads/sites',
134
- 'blogs.dir'
135
- );
136
-
137
- /**
138
- * Get user excluded folders
139
- */
140
- $directory = array();
141
- foreach ( $this->options->excludedDirectories as $dir ) {
142
- // Windows compatibility fix
143
- $dir = wpstg_replace_windows_directory_separator( $dir );
144
- $wpContentDir = wpstg_replace_windows_directory_separator( WP_CONTENT_DIR );
145
-
146
- if( strpos( $dir, $wpContentDir ) !== false ) {
147
- $directory[] = ltrim( str_replace( $wpContentDir, '', $dir ), '/\\' );
148
- }
149
- }
150
-
151
- $excludePaths = array_merge( $excludePaths, $directory );
152
-
153
- try {
154
-
155
- // Iterate over content directory
156
- $iterator = new \WPStaging\Iterators\RecursiveDirectoryIterator( WP_CONTENT_DIR );
157
-
158
- // Exclude sites, uploads, plugins or themes
159
- $iterator = new \WPStaging\Iterators\RecursiveFilterExclude( $iterator, apply_filters( 'wpstg_clone_mu_excl_folders', $excludePaths ) );
160
-
161
- // Recursively iterate over content directory
162
- $iterator = new \RecursiveIteratorIterator( $iterator, \RecursiveIteratorIterator::LEAVES_ONLY, \RecursiveIteratorIterator::CATCH_GET_CHILD );
163
-
164
- $this->log( "Scanning wp-content folder " . WP_CONTENT_DIR );
165
-
166
- // Write path line
167
- foreach ( $iterator as $item ) {
168
- if( $item->isFile() ) {
169
- $file = 'wp-content/' . $iterator->getSubPathName() . PHP_EOL;
170
- if( $this->write( $files, $file ) ) {
171
- $this->options->totalFiles++;
172
-
173
- // Add current file size
174
- $this->options->totalFileSize += $iterator->getSize();
175
- }
176
- }
177
- }
178
- } catch ( \Exception $e ) {
179
- $this->returnException( 'Error: ' . $e->getMessage() );
180
- //throw new \Exception( 'Error: ' . $e->getMessage() );
181
- }
182
-
183
- // close the file handler
184
- $this->close( $files );
185
- return true;
186
- }
187
-
188
- /**
189
- * Step 2
190
- * @return boolean
191
- * @throws \Exception
192
- */
193
- private function getWpIncludesFiles() {
194
-
195
- // Skip it
196
- if( $this->isDirectoryExcluded( \WPStaging\WPStaging::getWPpath() . 'wp-includes' ) ) {
197
- $this->log( "Skip " . \WPStaging\WPStaging::getWPpath() . 'wp-includes' );
198
- return true;
199
- }
200
-
201
- // open file handle and attach data to end of file
202
- $files = $this->open( $this->filename, 'a' );
203
-
204
- try {
205
-
206
- // Iterate over wp-admin directory
207
- $iterator = new \WPStaging\Iterators\RecursiveDirectoryIterator( \WPStaging\WPStaging::getWPpath() . 'wp-includes/' );
208
-
209
- // Recursively iterate over wp-includes directory
210
- $iterator = new \RecursiveIteratorIterator( $iterator, \RecursiveIteratorIterator::LEAVES_ONLY, \RecursiveIteratorIterator::CATCH_GET_CHILD );
211
-
212
- $this->log( "Scanning /wp-includes for its sub-directories and files" );
213
-
214
- // Write files
215
- foreach ( $iterator as $item ) {
216
- if( $item->isFile() ) {
217
- if( $this->write( $files, 'wp-includes/' . $iterator->getSubPathName() . PHP_EOL ) ) {
218
- $this->options->totalFiles++;
219
-
220
- // Add current file size
221
- $this->options->totalFileSize += $iterator->getSize();
222
- }
223
- }
224
- }
225
- } catch ( \Exception $e ) {
226
- $this->returnException( 'Error: ' . $e->getMessage() );
227
- }
228
-
229
- // close the file handler
230
- $this->close( $files );
231
- return true;
232
- }
233
-
234
- /**
235
- * Step 3
236
- * @return boolean
237
- * @throws \Exception
238
- */
239
- private function getWpAdminFiles() {
240
-
241
- // Skip it
242
- if( $this->isDirectoryExcluded( \WPStaging\WPStaging::getWPpath() . 'wp-admin/' ) ) {
243
- $this->log( "Skip " . \WPStaging\WPStaging::getWPpath() . 'wp-admin/' );
244
- return true;
245
- }
246
-
247
- // open file handle and attach data to end of file
248
- $files = $this->open( $this->filename, 'a' );
249
-
250
- try {
251
-
252
- // Iterate over wp-admin directory
253
- $iterator = new \WPStaging\Iterators\RecursiveDirectoryIterator( \WPStaging\WPStaging::getWPpath() . 'wp-admin/' );
254
-
255
- // Recursively iterate over content directory
256
- $iterator = new \RecursiveIteratorIterator( $iterator, \RecursiveIteratorIterator::LEAVES_ONLY, \RecursiveIteratorIterator::CATCH_GET_CHILD );
257
-
258
- $this->log( "Scanning /wp-admin for its sub-directories and files" );
259
-
260
- // Write path line
261
- foreach ( $iterator as $item ) {
262
- if( $item->isFile() ) {
263
- if( $this->write( $files, 'wp-admin/' . $iterator->getSubPathName() . PHP_EOL ) ) {
264
- $this->options->totalFiles++;
265
- // Too much cpu time
266
- //$this->options->totalFileSize += $iterator->getSize();
267
- }
268
- }
269
- }
270
- } catch ( \Exception $e ) {
271
- $this->returnException( 'Error: ' . $e->getMessage() );
272
- }
273
-
274
- // close the file handler
275
- $this->close( $files );
276
- return true;
277
- }
278
-
279
- /**
280
- * Step 4
281
- * Get WP Content Uploads Files multisite folder wp-content/uploads/sites or wp-content/blogs.dir/ID/files
282
- */
283
- private function getWpContentUploadsSites() {
284
-
285
- // Skip if main site is cloned
286
- if( is_main_site() ) {
287
- return true;
288
- }
289
-
290
- $blogId = get_current_blog_id();
291
-
292
- // Absolute path to uploads folder
293
- //$path = WP_CONTENT_DIR . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'sites' . DIRECTORY_SEPARATOR . $blogId . DIRECTORY_SEPARATOR;
294
- $path = $this->getAbsUploadPath();
295
-
296
- // Skip it
297
- if( !is_dir( $path ) ) {
298
- $this->log( "Skipping: {$path} does not exist." );
299
- return true;
300
- }
301
-
302
- // Skip it
303
- if( $this->isDirectoryExcluded( $path ) ) {
304
- $this->log( "Skipping: {$path}" );
305
- return true;
306
- }
307
-
308
-
309
- // open file handle
310
- $files = $this->open( $this->filename, 'a' );
311
-
312
- /**
313
- * Excluded folders relative to the folder to iterate
314
- */
315
- $excludePaths = array(
316
- 'cache',
317
- 'plugins/wps-hide-login',
318
- 'uploads/sites'
319
- );
320
-
321
- /**
322
- * Get user excluded folders
323
- */
324
- $directory = array();
325
- foreach ( $this->options->excludedDirectories as $dir ) {
326
- $path = wpstg_replace_windows_directory_separator( $path );
327
- $dir = wpstg_replace_windows_directory_separator( $dir );
328
- if( strpos( $dir, $path ) !== false ) {
329
- $directory[] = ltrim( str_replace( $path, '', $dir ), '/' );
330
- }
331
- }
332
-
333
- $excludePaths = array_merge( $excludePaths, $directory );
334
-
335
- try {
336
-
337
- // Iterate over content directory
338
- $iterator = new \WPStaging\Iterators\RecursiveDirectoryIterator( $path );
339
-
340
- // Exclude sites, uploads, plugins or themes
341
- $iterator = new \WPStaging\Iterators\RecursiveFilterExclude( $iterator, $excludePaths );
342
-
343
- // Recursively iterate over content directory
344
- $iterator = new \RecursiveIteratorIterator( $iterator, \RecursiveIteratorIterator::LEAVES_ONLY, \RecursiveIteratorIterator::CATCH_GET_CHILD );
345
- $uploadDir = wpstg_get_upload_dir();
346
- $this->log( "Scanning {$uploadDir} for its sub-directories and files..." );
347
-
348
- // Write path line
349
- foreach ( $iterator as $item ) {
350
- if( $item->isFile() ) {
351
- $file = $this->getRelUploadPath() . $iterator->getSubPathName() . PHP_EOL;
352
- if( $this->write( $files, $file ) ) {
353
- $this->options->totalFiles++;
354
-
355
- // Add current file size
356
- $this->options->totalFileSize += $iterator->getSize();
357
- }
358
- }
359
- }
360
- } catch ( \Exception $e ) {
361
- $this->returnException( 'Error: ' . $e->getMessage() );
362
- }
363
-
364
- // close the file handler
365
- $this->close( $files );
366
- return true;
367
- }
368
-
369
- /**
370
- * Get absolute path to the upload folder e.g. /srv/www/wp-content/blogs.dir/ID/files or /srv/www/wp-content/uploads/sites/ID/
371
- * @return type
372
- */
373
- private function getAbsUploadPath() {
374
- // Check first which method is used
375
- $uploads = wp_upload_dir();
376
- $basedir = $uploads['basedir'];
377
-
378
- return trailingslashit( $basedir );
379
- }
380
-
381
- /**
382
- * Get relative path to the upload folder like wp-content/uploads or wp-content/blogs.dir/2/files
383
- * @return string
384
- */
385
- private function getRelUploadPath() {
386
- $uploads = wp_upload_dir();
387
- $basedir = $uploads['basedir'];
388
-
389
- return trailingslashit( str_replace( wpstg_replace_windows_directory_separator(\WPStaging\WPStaging::getWPpath()), null, wpstg_replace_windows_directory_separator($basedir) ) );
390
- }
391
-
392
- /**
393
- * Step 5 - x
394
- * Get extra folders of the wp root level
395
- * Does not collect wp-includes, wp-admin and wp-content folder
396
- */
397
- private function getExtraFiles( $folder ) {
398
-
399
- if( !is_dir( $folder ) ) {
400
- return true;
401
- }
402
-
403
- // open file handle and attach data to end of file
404
- $files = $this->open( $this->filename, 'a' );
405
-
406
- try {
407
-
408
- // Iterate over extra directory
409
- $iterator = new \WPStaging\Iterators\RecursiveDirectoryIterator( $folder );
410
-
411
- $exclude = array();
412
-
413
- $iterator = new \WPStaging\Iterators\RecursiveFilterExclude( $iterator, $exclude );
414
- // Recursively iterate over content directory
415
- $iterator = new \RecursiveIteratorIterator( $iterator, \RecursiveIteratorIterator::LEAVES_ONLY, \RecursiveIteratorIterator::CATCH_GET_CHILD );
416
-
417
- $strings = new Strings();
418
- $this->log( "Scanning {$strings->getLastElemAfterString( '/', $folder )} for its sub-directories and files" );
419
-
420
- // Write path line
421
- foreach ( $iterator as $item ) {
422
- if( $item->isFile() ) {
423
- $path = str_replace( wpstg_replace_windows_directory_separator(\WPStaging\WPStaging::getWPpath()), '', wpstg_replace_windows_directory_separator($folder) ) . DIRECTORY_SEPARATOR . $iterator->getSubPathName() . PHP_EOL;
424
- if( $this->write( $files, $path ) ) {
425
- $this->options->totalFiles++;
426
- // Add current file size
427
- $this->options->totalFileSize += $iterator->getSize();
428
- }
429
- }
430
- }
431
- } catch ( \Exception $e ) {
432
- $this->returnException( 'Error: ' . $e->getMessage() );
433
- }
434
-
435
- // close the file handler
436
- $this->close( $files );
437
- return true;
438
- }
439
-
440
- /**
441
- * Closes a file handle
442
- *
443
- * @param resource $handle File handle to close
444
- * @return boolean
445
- */
446
- public function close( $handle ) {
447
- return @fclose( $handle );
448
- }
449
-
450
- /**
451
- * Opens a file in specified mode
452
- *
453
- * @param string $file Path to the file to open
454
- * @param string $mode Mode in which to open the file
455
- * @return resource
456
- * @throws Exception
457
- */
458
- public function open( $file, $mode ) {
459
-
460
- $file_handle = @fopen( $file, $mode );
461
- if( false === $file_handle ) {
462
- $this->returnException( sprintf( __( 'Unable to open %s with mode %s', 'wp-staging' ), $file, $mode ) );
463
- }
464
-
465
- return $file_handle;
466
- }
467
-
468
- /**
469
- * Write contents to a file
470
- *
471
- * @param resource $handle File handle to write to
472
- * @param string $content Contents to write to the file
473
- * @return integer
474
- * @throws Exception
475
- * @throws Exception
476
- */
477
- public function write( $handle, $content ) {
478
- $write_result = @fwrite( $handle, $content );
479
- if( false === $write_result ) {
480
- if( ( $meta = \stream_get_meta_data( $handle ) ) ) {
481
- throw new \Exception( sprintf( __( 'Unable to write to: %s', 'wp-staging' ), $meta['uri'] ) );
482
- }
483
- } elseif( strlen( $content ) !== $write_result ) {
484
- throw new \Exception( __( 'Out of disk space.', 'wp-staging' ) );
485
- }
486
-
487
- return $write_result;
488
- }
489
-
490
- /**
491
- * Execute the Current Step
492
- * Returns false when over threshold limits are hit or when the job is done, true otherwise
493
- * @return bool
494
- */
495
- protected function execute() {
496
-
497
- // No job left to execute
498
- if( $this->isFinished() ) {
499
- $this->prepareResponse( true, false );
500
- return false;
501
- }
502
-
503
-
504
- if( $this->options->currentStep == 0 ) {
505
- $this->getWpRootFiles();
506
- $this->prepareResponse( false, true );
507
- return false;
508
- }
509
-
510
- if( $this->options->currentStep == 1 ) {
511
- $this->getWpContentFiles();
512
- $this->prepareResponse( false, true );
513
- return false;
514
- }
515
-
516
- if( $this->options->currentStep == 2 ) {
517
- $this->getWpIncludesFiles();
518
- $this->prepareResponse( false, true );
519
- return false;
520
- }
521
-
522
- if( $this->options->currentStep == 3 ) {
523
- $this->getWpAdminFiles();
524
- $this->prepareResponse( false, true );
525
- return false;
526
- }
527
-
528
- if( $this->options->currentStep == 4 ) {
529
- $this->getWpContentUploadsSites();
530
- $this->prepareResponse( false, true );
531
- return false;
532
- }
533
-
534
- if( isset( $this->options->extraDirectories[$this->options->currentStep - $this->total] ) ) {
535
- $this->getExtraFiles( $this->options->extraDirectories[$this->options->currentStep - $this->total] );
536
- $this->prepareResponse( false, true );
537
- return false;
538
- }
539
-
540
-
541
- // Prepare response
542
- $this->prepareResponse( false, true );
543
- // Not finished
544
- return true;
545
- }
546
-
547
- /**
548
- * Checks Whether There is Any Job to Execute or Not
549
- * @return bool
550
- */
551
- protected function isFinished() {
552
- if( $this->options->currentStep >= $this->options->totalSteps ) {
553
- return true;
554
- }
555
- }
556
-
557
- /**
558
- * Save files
559
- * @return bool
560
- */
561
- protected function saveProgress() {
562
- return $this->saveOptions();
563
- }
564
-
565
- /**
566
- * Get files
567
- * @return void
568
- */
569
- protected function getFiles() {
570
- $fileName = $this->cache->getCacheDir() . "files_to_copy." . $this->cache->getCacheExtension();
571
-
572
- if( false === ($this->files = @file_get_contents( $fileName )) ) {
573
- $this->files = array();
574
- return;
575
- }
576
-
577
- $this->files = explode( PHP_EOL, $this->files );
578
- }
579
-
580
- /**
581
- * Replace forward slash with backslash directory separator
582
- *
583
- * @param string $path Path
584
- *
585
- * @return string
586
- */
587
- // private function sanitizeDirectorySeparator( $path ) {
588
- // $string = str_replace( "/", "\\", $path );
589
- // return str_replace( '\\\\', '\\', $string );
590
- // }
591
-
592
- /**
593
- * Check if directory is excluded
594
- * @param string $directory
595
- * @return bool
596
- */
597
- protected function isDirectoryExcluded( $directory ) {
598
- $directory = wpstg_replace_windows_directory_separator( $directory );
599
- foreach ( $this->options->excludedDirectories as $excludedDirectory ) {
600
- $excludedDirectory = wpstg_replace_windows_directory_separator( $excludedDirectory );
601
- if( strpos( trailingslashit( $directory ), trailingslashit( $excludedDirectory ) ) === 0 ) {
602
- return true;
603
- }
604
- }
605
-
606
- return false;
607
- }
608
-
609
- }
1
+ <?php
2
+
3
+ namespace WPStaging\Backend\Modules\Jobs\Multisite;
4
+
5
+ // No Direct Access
6
+ if( !defined( "WPINC" ) ) {
7
+ die;
8
+ }
9
+
10
+ use WPStaging\WPStaging;
11
+ use WPStaging\Utils\Logger;
12
+ use WPStaging\Utils\Strings;
13
+ use WPStaging\Iterators\RecursiveDirectoryIterator;
14
+ use WPStaging\Iterators\RecursiveFilterExclude;
15
+ use WPStaging\Backend\Modules\Jobs\JobExecutable;
16
+
17
+ /**
18
+ * Class Files
19
+ * @package WPStaging\Backend\Modules\Directories
20
+ */
21
+ class Directories extends JobExecutable {
22
+
23
+ /**
24
+ * @var array
25
+ */
26
+ private $files = array();
27
+
28
+ /**
29
+ * Total steps to do
30
+ * @var int
31
+ */
32
+ private $total = 5;
33
+
34
+ /**
35
+ * path to the cache file
36
+ * @var string
37
+ */
38
+ private $filename;
39
+
40
+ /**
41
+ * Initialize
42
+ */
43
+ public function initialize() {
44
+ $this->filename = $this->cache->getCacheDir() . "files_to_copy." . $this->cache->getCacheExtension();
45
+ }
46
+
47
+ /**
48
+ * Calculate Total Steps in This Job and Assign It to $this->options->totalSteps
49
+ * @return void
50
+ */
51
+ protected function calculateTotalSteps() {
52
+
53
+ $this->options->totalSteps = $this->total + count( $this->options->extraDirectories );
54
+ }
55
+
56
+ /**
57
+ * Start Module
58
+ * @return object
59
+ */
60
+ public function start() {
61
+
62
+ // Execute steps
63
+ $this->run();
64
+
65
+ // Save option, progress
66
+ $this->saveProgress();
67
+
68
+ return ( object ) $this->response;
69
+ }
70
+
71
+ /**
72
+ * Step 0
73
+ * Get WP Root files
74
+ * Does not collect any sub folders
75
+ */
76
+ private function getWpRootFiles() {
77
+
78
+ // Skip it
79
+ if( $this->isDirectoryExcluded( \WPStaging\WPStaging::getWPpath() . 'wp-admin' . DIRECTORY_SEPARATOR ) ) {
80
+ $this->log( "Skip " . \WPStaging\WPStaging::getWPpath() . 'wp-admin' . DIRECTORY_SEPARATOR );
81
+ return true;
82
+ }
83
+
84
+ // open file handle
85
+ $files = $this->open( $this->filename, 'a' );
86
+
87
+ try {
88
+
89
+ // Iterate over wp root directory
90
+ $iterator = new \DirectoryIterator( \WPStaging\WPStaging::getWPpath() );
91
+
92
+ $this->log( "Scanning / for files" );
93
+
94
+ // Write path line
95
+ foreach ( $iterator as $item ) {
96
+ if( !$item->isDot() && $item->isFile() ) {
97
+ if( $this->write( $files, $iterator->getFilename() . PHP_EOL ) ) {
98
+ $this->options->totalFiles++;
99
+
100
+ // Too much cpu time
101
+ //$this->options->totalFileSize += $iterator->getSize();
102
+ }
103
+ }
104
+ }
105
+ } catch ( \Exception $e ) {
106
+ $this->returnException( 'Error: ' . $e->getMessage() );
107
+ }
108
+
109
+ $this->close( $files );
110
+ return true;
111
+ }
112
+
113
+ /**
114
+ * Step 2
115
+ * Get WP Content Files without multisite folder wp-content/uploads/sites or wp-content/blogs.dir/
116
+ */
117
+ private function getWpContentFiles() {
118
+
119
+ // Skip it
120
+ if( $this->isDirectoryExcluded( WP_CONTENT_DIR ) ) {
121
+ $this->log( "Skip " . WP_CONTENT_DIR );
122
+ return true;
123
+ }
124
+ // open file handle
125
+ $files = $this->open( $this->filename, 'a' );
126
+
127
+ /**
128
+ * Excluded folders relative to the folder to iterate
129
+ */
130
+ $excludePaths = array(
131
+ 'cache',
132
+ 'plugins/wps-hide-login',
133
+ 'uploads/sites',
134
+ 'blogs.dir'
135
+ );
136
+
137
+ /**
138
+ * Get user excluded folders
139
+ */
140
+ $directory = array();
141
+ foreach ( $this->options->excludedDirectories as $dir ) {
142
+ // Windows compatibility fix
143
+ $dir = wpstg_replace_windows_directory_separator( $dir );
144
+ $wpContentDir = wpstg_replace_windows_directory_separator( WP_CONTENT_DIR );
145
+
146
+ if( strpos( $dir, $wpContentDir ) !== false ) {
147
+ $directory[] = ltrim( str_replace( $wpContentDir, '', $dir ), '/\\' );
148
+ }
149
+ }
150
+
151
+ $excludePaths = array_merge( $excludePaths, $directory );
152
+
153
+ try {
154
+
155
+ // Iterate over content directory
156
+ $iterator = new \WPStaging\Iterators\RecursiveDirectoryIterator( WP_CONTENT_DIR );
157
+
158
+ // Exclude sites, uploads, plugins or themes
159
+ $iterator = new \WPStaging\Iterators\RecursiveFilterExclude( $iterator, apply_filters( 'wpstg_clone_mu_excl_folders', $excludePaths ) );
160
+
161
+ // Recursively iterate over content directory
162
+ $iterator = new \RecursiveIteratorIterator( $iterator, \RecursiveIteratorIterator::LEAVES_ONLY, \RecursiveIteratorIterator::CATCH_GET_CHILD );
163
+
164
+ $this->log( "Scanning wp-content folder " . WP_CONTENT_DIR );
165
+
166
+ // Write path line
167
+ foreach ( $iterator as $item ) {
168
+ if( $item->isFile() ) {
169
+ $file = 'wp-content/' . $iterator->getSubPathName() . PHP_EOL;
170
+ if( $this->write( $files, $file ) ) {
171
+ $this->options->totalFiles++;
172
+
173
+ // Add current file size
174
+ $this->options->totalFileSize += $iterator->getSize();
175
+ }
176
+ }
177
+ }
178
+ } catch ( \Exception $e ) {
179
+ $this->returnException( 'Error: ' . $e->getMessage() );
180
+ //throw new \Exception( 'Error: ' . $e->getMessage() );
181
+ }
182
+
183
+ // close the file handler
184
+ $this->close( $files );
185
+ return true;
186
+ }
187
+
188
+ /**
189
+ * Step 2
190
+ * @return boolean
191
+ * @throws \Exception
192
+ */
193
+ private function getWpIncludesFiles() {
194
+
195
+ // Skip it
196
+ if( $this->isDirectoryExcluded( \WPStaging\WPStaging::getWPpath() . 'wp-includes' ) ) {
197
+ $this->log( "Skip " . \WPStaging\WPStaging::getWPpath() . 'wp-includes' );
198
+ return true;
199
+ }
200
+
201
+ // open file handle and attach data to end of file
202
+ $files = $this->open( $this->filename, 'a' );
203
+
204
+ try {
205
+
206
+ // Iterate over wp-admin directory
207
+ $iterator = new \WPStaging\Iterators\RecursiveDirectoryIterator( \WPStaging\WPStaging::getWPpath() . 'wp-includes/' );
208
+
209
+ // Recursively iterate over wp-includes directory
210
+ $iterator = new \RecursiveIteratorIterator( $iterator, \RecursiveIteratorIterator::LEAVES_ONLY, \RecursiveIteratorIterator::CATCH_GET_CHILD );
211
+
212
+ $this->log( "Scanning /wp-includes for its sub-directories and files" );
213
+
214
+ // Write files
215
+ foreach ( $iterator as $item ) {
216
+ if( $item->isFile() ) {
217
+ if( $this->write( $files, 'wp-includes/' . $iterator->getSubPathName() . PHP_EOL ) ) {
218
+ $this->options->totalFiles++;
219
+
220
+ // Add current file size
221
+ $this->options->totalFileSize += $iterator->getSize();
222
+ }
223
+ }
224
+ }
225
+ } catch ( \Exception $e ) {
226
+ $this->returnException( 'Error: ' . $e->getMessage() );
227
+ }
228
+
229
+ // close the file handler
230
+ $this->close( $files );
231
+ return true;
232
+ }
233
+
234
+ /**
235
+ * Step 3
236
+ * @return boolean
237
+ * @throws \Exception
238
+ */
239
+ private function getWpAdminFiles() {
240
+
241
+ // Skip it
242
+ if( $this->isDirectoryExcluded( \WPStaging\WPStaging::getWPpath() . 'wp-admin/' ) ) {
243
+ $this->log( "Skip " . \WPStaging\WPStaging::getWPpath() . 'wp-admin/' );
244
+ return true;
245
+ }
246
+
247
+ // open file handle and attach data to end of file
248
+ $files = $this->open( $this->filename, 'a' );
249
+
250
+ try {
251
+
252
+ // Iterate over wp-admin directory
253
+ $iterator = new \WPStaging\Iterators\RecursiveDirectoryIterator( \WPStaging\WPStaging::getWPpath() . 'wp-admin/' );
254
+
255
+ // Recursively iterate over content directory
256
+ $iterator = new \RecursiveIteratorIterator( $iterator, \RecursiveIteratorIterator::LEAVES_ONLY, \RecursiveIteratorIterator::CATCH_GET_CHILD );
257
+
258
+ $this->log( "Scanning /wp-admin for its sub-directories and files" );
259
+
260
+ // Write path line
261
+ foreach ( $iterator as $item ) {
262
+ if( $item->isFile() ) {
263
+ if( $this->write( $files, 'wp-admin/' . $iterator->getSubPathName() . PHP_EOL ) ) {
264
+ $this->options->totalFiles++;
265
+ // Too much cpu time
266
+ //$this->options->totalFileSize += $iterator->getSize();
267
+ }
268
+ }
269
+ }
270
+ } catch ( \Exception $e ) {
271
+ $this->returnException( 'Error: ' . $e->getMessage() );
272
+ }
273
+
274
+ // close the file handler
275
+ $this->close( $files );
276
+ return true;
277
+ }
278
+
279
+ /**
280
+ * Step 4
281
+ * Get WP Content Uploads Files multisite folder wp-content/uploads/sites or wp-content/blogs.dir/ID/files
282
+ */
283
+ private function getWpContentUploadsSites() {
284
+
285
+ // Skip if main site is cloned
286
+ if( is_main_site() ) {
287
+ return true;
288
+ }
289
+
290
+ $blogId = get_current_blog_id();
291
+
292
+ // Absolute path to uploads folder
293
+ //$path = WP_CONTENT_DIR . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'sites' . DIRECTORY_SEPARATOR . $blogId . DIRECTORY_SEPARATOR;
294
+ $path = $this->getAbsUploadPath();
295
+
296
+ // Skip it
297
+ if( !is_dir( $path ) ) {
298
+ $this->log( "Skipping: {$path} does not exist." );
299
+ return true;
300
+ }
301
+
302
+ // Skip it
303
+ if( $this->isDirectoryExcluded( $path ) ) {
304
+ $this->log( "Skipping: {$path}" );
305
+ return true;
306
+ }
307
+
308
+
309
+ // open file handle
310
+ $files = $this->open( $this->filename, 'a' );
311
+
312
+ /**
313
+ * Excluded folders relative to the folder to iterate
314
+ */
315
+ $excludePaths = array(
316
+ 'cache',
317
+ 'plugins/wps-hide-login',
318
+ 'uploads/sites'
319
+ );
320
+
321
+ /**
322
+ * Get user excluded folders
323
+ */
324
+ $directory = array();
325
+ foreach ( $this->options->excludedDirectories as $dir ) {
326
+ $path = wpstg_replace_windows_directory_separator( $path );
327
+ $dir = wpstg_replace_windows_directory_separator( $dir );
328
+ if( strpos( $dir, $path ) !== false ) {
329
+ $directory[] = ltrim( str_replace( $path, '', $dir ), '/' );
330
+ }
331
+ }
332
+
333
+ $excludePaths = array_merge( $excludePaths, $directory );
334
+
335
+ try {
336
+
337
+ // Iterate over content directory
338
+ $iterator = new \WPStaging\Iterators\RecursiveDirectoryIterator( $path );
339
+
340
+ // Exclude sites, uploads, plugins or themes
341
+ $iterator = new \WPStaging\Iterators\RecursiveFilterExclude( $iterator, $excludePaths );
342
+
343
+ // Recursively iterate over content directory
344
+ $iterator = new \RecursiveIteratorIterator( $iterator, \RecursiveIteratorIterator::LEAVES_ONLY, \RecursiveIteratorIterator::CATCH_GET_CHILD );
345
+ $uploadDir = wpstg_get_upload_dir();
346
+ $this->log( "Scanning {$uploadDir} for its sub-directories and files..." );
347
+
348
+ // Write path line
349
+ foreach ( $iterator as $item ) {
350
+ if( $item->isFile() ) {
351
+ $file = $this->getRelUploadPath() . $iterator->getSubPathName() . PHP_EOL;
352
+ if( $this->write( $files, $file ) ) {
353
+ $this->options->totalFiles++;
354
+
355
+ // Add current file size
356
+ $this->options->totalFileSize += $iterator->getSize();
357
+ }
358
+ }
359
+ }
360
+ } catch ( \Exception $e ) {
361
+ $this->returnException( 'Error: ' . $e->getMessage() );
362
+ }
363
+
364
+ // close the file handler
365
+ $this->close( $files );
366
+ return true;
367
+ }
368
+
369
+ /**
370
+ * Get absolute path to the upload folder e.g. /srv/www/wp-content/blogs.dir/ID/files or /srv/www/wp-content/uploads/sites/ID/
371
+ * @return type
372
+ */
373
+ private function getAbsUploadPath() {
374
+ // Check first which method is used
375
+ $uploads = wp_upload_dir();
376
+ $basedir = $uploads['basedir'];
377
+
378
+ return trailingslashit( $basedir );
379
+ }
380
+
381
+ /**
382
+ * Get relative path to the upload folder like wp-content/uploads or wp-content/blogs.dir/2/files
383
+ * @return string
384
+ */
385
+ private function getRelUploadPath() {
386
+ $uploads = wp_upload_dir();
387
+ $basedir = $uploads['basedir'];
388
+
389
+ return trailingslashit( str_replace( wpstg_replace_windows_directory_separator(\WPStaging\WPStaging::getWPpath()), null, wpstg_replace_windows_directory_separator($basedir) ) );
390
+ }
391
+
392
+ /**
393
+ * Step 5 - x
394
+ * Get extra folders of the wp root level
395
+ * Does not collect wp-includes, wp-admin and wp-content folder
396
+ */
397
+ private function getExtraFiles( $folder ) {
398
+
399
+ if( !is_dir( $folder ) ) {
400
+ return true;
401
+ }
402
+
403
+ // open file handle and attach data to end of file
404
+ $files = $this->open( $this->filename, 'a' );
405
+
406
+ try {
407
+
408
+ // Iterate over extra directory
409
+ $iterator = new \WPStaging\Iterators\RecursiveDirectoryIterator( $folder );
410
+
411
+ $exclude = array();
412
+
413
+ $iterator = new \WPStaging\Iterators\RecursiveFilterExclude( $iterator, $exclude );
414
+ // Recursively iterate over content directory
415
+ $iterator = new \RecursiveIteratorIterator( $iterator, \RecursiveIteratorIterator::LEAVES_ONLY, \RecursiveIteratorIterator::CATCH_GET_CHILD );
416
+
417
+ $strings = new Strings();
418
+ $this->log( "Scanning {$strings->getLastElemAfterString( '/', $folder )} for its sub-directories and files" );
419
+
420
+ // Write path line
421
+ foreach ( $iterator as $item ) {
422
+ if( $item->isFile() ) {
423
+ $path = str_replace( wpstg_replace_windows_directory_separator(\WPStaging\WPStaging::getWPpath()), '', wpstg_replace_windows_directory_separator($folder) ) . DIRECTORY_SEPARATOR . $iterator->getSubPathName() . PHP_EOL;
424
+ if( $this->write( $files, $path ) ) {
425
+ $this->options->totalFiles++;
426
+ // Add current file size
427
+ $this->options->totalFileSize += $iterator->getSize();
428
+ }
429
+ }
430
+ }
431
+ } catch ( \Exception $e ) {
432
+ $this->returnException( 'Error: ' . $e->getMessage() );
433
+ }
434
+
435
+ // close the file handler
436
+ $this->close( $files );
437
+ return true;
438
+ }
439
+
440
+ /**
441
+ * Closes a file handle
442
+ *
443
+ * @param resource $handle File handle to close
444
+ * @return boolean
445
+ */
446
+ public function close( $handle ) {
447
+ return @fclose( $handle );
448
+ }
449
+
450
+ /**
451
+ * Opens a file in specified mode
452
+ *
453
+ * @param string $file Path to the file to open
454
+ * @param string $mode Mode in which to open the file
455
+ * @return resource
456
+ * @throws Exception
457
+ */
458
+ public function open( $file, $mode ) {
459
+
460
+ $file_handle = @fopen( $file, $mode );
461
+ if( false === $file_handle ) {
462
+ $this->returnException( sprintf( __( 'Unable to open %s with mode %s', 'wp-staging' ), $file, $mode ) );
463
+ }
464
+
465
+ return $file_handle;
466
+ }
467
+
468
+ /**
469
+ * Write contents to a file
470
+ *
471
+ * @param resource $handle File handle to write to
472
+ * @param string $content Contents to write to the file
473
+ * @return integer
474
+ * @throws Exception
475
+ * @throws Exception
476
+ */
477
+ public function write( $handle, $content ) {
478
+ $write_result = @fwrite( $handle, $content );
479
+ if( false === $write_result ) {
480
+ if( ( $meta = \stream_get_meta_data( $handle ) ) ) {
481
+ throw new \Exception( sprintf( __( 'Unable to write to: %s', 'wp-staging' ), $meta['uri'] ) );
482
+ }
483
+ } elseif( strlen( $content ) !== $write_result ) {
484
+ throw new \Exception( __( 'Out of disk space.', 'wp-staging' ) );
485
+ }
486
+
487
+ return $write_result;
488
+ }
489
+
490
+ /**
491
+ * Execute the Current Step
492
+ * Returns false when over threshold limits are hit or when the job is done, true otherwise
493
+ * @return bool
494
+ */
495
+ protected function execute() {
496
+
497
+ // No job left to execute
498
+ if( $this->isFinished() ) {
499
+ $this->prepareResponse( true, false );
500
+ return false;
501
+ }
502
+
503
+
504
+ if( $this->options->currentStep == 0 ) {
505
+ $this->getWpRootFiles();
506
+ $this->prepareResponse( false, true );
507
+ return false;
508
+ }
509
+
510
+ if( $this->options->currentStep == 1 ) {
511
+ $this->getWpContentFiles();
512
+ $this->prepareResponse( false, true );
513
+ return false;
514
+ }
515
+
516
+ if( $this->options->currentStep == 2 ) {
517
+ $this->getWpIncludesFiles();
518
+ $this->prepareResponse( false, true );
519
+ return false;
520
+ }
521
+
522
+ if( $this->options->currentStep == 3 ) {
523
+ $this->getWpAdminFiles();
524
+ $this->prepareResponse( false, true );
525
+ return false;
526
+ }
527
+
528
+ if( $this->options->currentStep == 4 ) {
529
+ $this->getWpContentUploadsSites();
530
+ $this->prepareResponse( false, true );
531
+ return false;
532
+ }
533
+
534
+ if( isset( $this->options->extraDirectories[$this->options->currentStep - $this->total] ) ) {
535
+ $this->getExtraFiles( $this->options->extraDirectories[$this->options->currentStep - $this->total] );
536
+ $this->prepareResponse( false, true );
537
+ return false;
538
+ }
539
+
540
+
541
+ // Prepare response
542
+ $this->prepareResponse( false, true );
543
+ // Not finished
544
+ return true;
545
+ }
546
+
547
+ /**
548
+ * Checks Whether There is Any Job to Execute or Not
549
+ * @return bool
550
+ */
551
+ protected function isFinished() {
552
+ if( $this->options->currentStep >= $this->options->totalSteps ) {
553
+ return true;
554
+ }
555
+ }
556
+
557
+ /**
558
+ * Save files
559
+ * @return bool
560
+ */
561
+ protected function saveProgress() {
562
+ return $this->saveOptions();
563
+ }
564
+
565
+ /**
566
+ * Get files
567
+ * @return void
568
+ */
569
+ protected function getFiles() {
570
+ $fileName = $this->cache->getCacheDir() . "files_to_copy." . $this->cache->getCacheExtension();
571
+
572
+ if( false === ($this->files = @file_get_contents( $fileName )) ) {
573
+ $this->files = array();
574
+ return;
575
+ }
576
+
577
+ $this->files = explode( PHP_EOL, $this->files );
578
+ }
579
+
580
+ /**
581
+ * Replace forward slash with backslash directory separator
582
+ *
583
+ * @param string $path Path
584
+ *
585
+ * @return string
586
+ */
587
+ // private function sanitizeDirectorySeparator( $path ) {
588
+ // $string = str_replace( "/", "\\", $path );
589
+ // return str_replace( '\\\\', '\\', $string );
590
+ // }
591
+
592
+ /**
593
+ * Check if directory is excluded
594
+ * @param string $directory
595
+ * @return bool
596
+ */
597
+ protected function isDirectoryExcluded( $directory ) {
598
+ $directory = wpstg_replace_windows_directory_separator( $directory );
599
+ foreach ( $this->options->excludedDirectories as $excludedDirectory ) {
600
+ $excludedDirectory = wpstg_replace_windows_directory_separator( $excludedDirectory );
601
+ if( strpos( trailingslashit( $directory ), trailingslashit( $excludedDirectory ) ) === 0 ) {
602
+ return true;
603
+ }
604
+ }
605
+
606
+ return false;
607
+ }
608
+
609
+ }
apps/Backend/Modules/Jobs/Multisite/Files.php CHANGED
@@ -1,481 +1,481 @@
1
- <?php
2
-
3
- namespace WPStaging\Backend\Modules\Jobs\Multisite;
4
-
5
- use WPStaging\Backend\Modules\Jobs\JobExecutable;
6
- // No Direct Access
7
- use WPStaging\Utils\Logger;
8
-
9
- if( !defined( "WPINC" ) ) {
10
- die;
11
- }
12
-
13
- /**
14
- * Class Files
15
- * @package WPStaging\Backend\Modules\Jobs
16
- */
17
- class Files extends JobExecutable {
18
-
19
- /**
20
- * @var \SplFileObject
21
- */
22
- private $file;
23
-
24
- /**
25
- * @var int
26
- */
27
- private $maxFilesPerRun;
28
-
29
- /**
30
- * @var string
31
- */
32
- private $destination;
33
-
34
- /**
35
- * Initialization
36
- */
37
- public function initialize() {
38
-
39
- $this->destination = $this->options->destinationDir;
40
-
41
- $filePath = $this->cache->getCacheDir() . "files_to_copy." . $this->cache->getCacheExtension();
42
-
43
- if( is_file( $filePath ) ) {
44
- $this->file = new \SplFileObject( $filePath, 'r' );
45
- }
46
-
47
- // Informational logs
48
- if( 0 == $this->options->currentStep ) {
49
- $this->log( "Copying files..." );
50
- }
51
-
52
- $this->settings->batchSize = $this->settings->batchSize * 1000000;
53
- $this->maxFilesPerRun = $this->settings->fileLimit;
54
- //$this->maxFilesPerRun = ($this->settings->cpuLoad === 'low') ? 50 : 1;
55
- }
56
-
57
- /**
58
- * Calculate Total Steps in This Job and Assign It to $this->options->totalSteps
59
- * @return void
60
- */
61
- protected function calculateTotalSteps() {
62
- $this->options->totalSteps = ceil( $this->options->totalFiles / $this->maxFilesPerRun );
63
- }
64
-
65
- /**
66
- * Execute the Current Step
67
- * Returns false when over threshold limits are hit or when the job is done, true otherwise
68
- * @return bool
69
- */
70
- protected function execute() {
71
- // Finished
72
- if( $this->isFinished() ) {
73
- $this->log( "Copying files finished" );
74
- $this->prepareResponse( true, false );
75
- return false;
76
- }
77
-
78
- // Get files and copy'em
79
- if( !$this->getFilesAndCopy() ) {
80
- $this->prepareResponse( false, false );
81
- return false;
82
- }
83
-
84
- // Prepare and return response
85
- $this->prepareResponse();
86
-
87
- // Not finished
88
- return true;
89
- }
90
-
91
- /**
92
- * Get files and copy
93
- * @return bool
94
- */
95
- private function getFilesAndCopy() {
96
- // Over limits threshold
97
- if( $this->isOverThreshold() ) {
98
- // Prepare response and save current progress
99
- $this->prepareResponse( false, false );
100
- $this->saveOptions();
101
- return false;
102
- }
103
-
104
- // Go to last copied line and than to next one
105
- //if ($this->options->copiedFiles != 0) {
106
- if( isset( $this->options->copiedFiles ) && $this->options->copiedFiles != 0 ) {
107
- $this->file->seek( $this->options->copiedFiles - 1 );
108
- }
109
-
110
- $this->file->setFlags( \SplFileObject::SKIP_EMPTY | \SplFileObject::READ_AHEAD );
111
-
112
- // Start time
113
- //$start = microtime( true );
114
- // Loop x files at a time
115
- //$this->maxFilesPerRun = 300;
116
- for ( $i = 0; $i < $this->maxFilesPerRun; $i++ ) {
117
-
118
- // Reached timeout
119
- // if( ( $timeout = apply_filters( 'wpstg_job_timeout', 10 ) ) ) {
120
- // if( ( \microtime( true ) - $start ) > $timeout ) {
121
- // // Prepare response and save current progress
122
- // $this->prepareResponse( false, true );
123
- // $this->saveOptions();
124
- // return false;
125
- // }
126
- // }
127
- // Increment copied files
128
- // Do this anytime to make sure to not stuck in the same step / files
129
- $this->options->copiedFiles++;
130
-
131
- // End of file
132
- if( $this->file->eof() ) {
133
- break;
134
- }
135
-
136
- $file = $this->file->fgets();
137
-
138
-
139
- $this->copyFile( $file );
140
- }
141
-
142
-
143
-
144
- $totalFiles = $this->options->copiedFiles;
145
- // Log this only every 50 entries to keep the log small and to not block the rendering browser
146
- if( $this->options->copiedFiles % 50 == 0 ) {
147
- $this->log( "Total {$totalFiles} files processed" );
148
- }
149
-
150
- return true;
151
- }
152
-
153
- /**
154
- * Checks Whether There is Any Job to Execute or Not
155
- * @return bool
156
- */
157
- private function isFinished() {
158
- return (
159
- !isset( $this->options->isRunning ) ||
160
- $this->options->currentStep > $this->options->totalSteps ||
161
- $this->options->copiedFiles >= $this->options->totalFiles
162
- );
163
- }
164
-
165
- /**
166
- * @param string $file
167
- * @return bool
168
- */
169
- private function copyFile( $file ) {
170
-
171
- $file = trim( \WPStaging\WPStaging::getWPpath() . $file );
172
-
173
- $file = wpstg_replace_windows_directory_separator($file);
174
-
175
- $directory = dirname( $file );
176
-
177
- // Directory is excluded
178
- if( $this->isDirectoryExcluded( $directory ) ) {
179
- $this->debugLog( "Skipping directory by rule: {$file}", Logger::TYPE_INFO );
180
- return false;
181
- }
182
-
183
- // File is excluded
184
- if( $this->isFileExcluded( $file ) ) {
185
- $this->debugLog( "Skipping file by rule: {$file}", Logger::TYPE_INFO );
186
- return false;
187
- }
188
- // Path + File is excluded
189
- if( $this->isFileExcludedFullPath( $file ) ) {
190
- $this->debugLog( "Skipping file by rule: {$file}", Logger::TYPE_INFO );
191
- return false;
192
- }
193
-
194
- // Invalid file, skipping it as if succeeded
195
- if( !is_file( $file ) ) {
196
- $this->log( "File doesn't exist {$file}", Logger::TYPE_WARNING );
197
- return true;
198
- }
199
- // Invalid file, skipping it as if succeeded
200
- if( !is_readable( $file ) ) {
201
- $this->log( "Can't read file {$file}", Logger::TYPE_WARNING );
202
- return true;
203
- }
204
-
205
-
206
- // Get file size
207
- $fileSize = filesize( $file );
208
-
209
- // File is over maximum allowed file size (8MB)
210
- if( $fileSize >= $this->settings->maxFileSize * 1000000 ) {
211
- $this->log( "Skipping big file: {$file}", Logger::TYPE_INFO );
212
- return false;
213
- }
214
-
215
- // Failed to get destination
216
- if( false === ($destination = $this->getDestination( $file )) ) {
217
- $this->log( "Can't get the destination of {$file}", Logger::TYPE_WARNING );
218
- return false;
219
- }
220
-
221
- // File is over batch size
222
- if( $fileSize >= $this->settings->batchSize ) {
223
- $this->log( "Trying to copy big file: {$file} -> {$destination}", Logger::TYPE_INFO );
224
- return $this->copyBig( $file, $destination, $this->settings->batchSize );
225
- }
226
-
227
- // Attempt to copy
228
- if( !@copy( $file, $destination ) ) {
229
- $errors = error_get_last();
230
- $this->log( "Files: Failed to copy file to destination. Error: {$errors['message']} {$file} -> {$destination}", Logger::TYPE_ERROR );
231
- return false;
232
- }
233
-
234
- // Set file permissions
235
- @chmod( $destination, wpstg_get_permissions_for_file() );
236
-
237
- $this->setDirPermissions( $destination );
238
-
239
- return true;
240
- }
241
-
242
- /**
243
- * Set directory permissions
244
- * @param type $file
245
- * @return boolean
246
- */
247
- private function setDirPermissions( $file ) {
248
- $dir = dirname( $file );
249
- if( is_dir( $dir ) ) {
250
- @chmod( $dir, wpstg_get_permissions_for_directory() );
251
- }
252
- return false;
253
- }
254
-
255
- /**
256
- * Gets destination file and checks if the directory exists, if it does not attempts to create it.
257
- * If creating destination directory fails, it returns false, gives destination full path otherwise
258
- * @param string $file
259
- * @return bool|string
260
- */
261
- private function getDestination( $file ) {
262
- //$file = $this->getMultisiteUploadFolder( $file );
263
- $file = wpstg_replace_windows_directory_separator($file);
264
- $rootPath = wpstg_replace_windows_directory_separator(\WPStaging\WPStaging::getWPpath());
265
- $relativePath = str_replace( $rootPath, null, $file );
266
- $destinationPath = $this->destination . $relativePath;
267
- $destinationDirectory = dirname( $destinationPath );
268
-
269
- if( !is_dir( $destinationDirectory ) && !@mkdir( $destinationDirectory, wpstg_get_permissions_for_directory(), true ) ) {
270
- $this->log( "Files: Can not create directory {$destinationDirectory}", Logger::TYPE_ERROR );
271
- return false;
272
- }
273
-
274
- return $this->sanitizeDirectorySeparator( $destinationPath );
275
- }
276
-
277
- /**
278
- * Replace relative path of file if its located in multisite upload folder
279
- * wp-content/uploads/sites/SITEID or old wordpress structure wp-content/blogs.dir/SITEID/files
280
- * @return boolean
281
- */
282
- private function getMultisiteUploadFolder( $file ) {
283
- // Check first which method is used
284
- $uploads = wp_upload_dir();
285
- $basedir = $uploads['basedir'];
286
-
287
- if( false === strpos( $basedir, 'blogs.dir' ) ) {
288
- // Since WP 3.5
289
- $search = 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'sites' . DIRECTORY_SEPARATOR . get_current_blog_id();
290
- $replace = 'wp-content' . DIRECTORY_SEPARATOR . 'uploads';
291
- $uploadsFolder = str_replace( $search, $replace, $file );
292
- } else {
293
- // old blog structure
294
- $search = 'wp-content' . DIRECTORY_SEPARATOR . 'blogs.dir' . DIRECTORY_SEPARATOR . get_current_blog_id() . DIRECTORY_SEPARATOR . 'files';
295
- $replace = 'wp-content' . DIRECTORY_SEPARATOR . 'uploads';
296
- $uploadsFolder = str_replace( $search, $replace, $file );
297
- }
298
-
299
- return $uploadsFolder;
300
- }
301
-
302
- /**
303
- * Copy bigger files than $this->settings->batchSize
304
- * @param string $src
305
- * @param string $dst
306
- * @param int $buffersize
307
- * @return boolean
308
- */
309
- private function copyBig( $src, $dst, $buffersize ) {
310
- $src = fopen( $src, 'r' );
311
- $dest = fopen( $dst, 'w' );
312
-
313
- // Try first method:
314
- while ( !feof( $src ) ) {
315
- if( false === fwrite( $dest, fread( $src, $buffersize ) ) ) {
316
- $error = true;
317
- }
318
- }
319
- // Try second method if first one failed
320
- if( isset( $error ) && ($error === true) ) {
321
- while ( !feof( $src ) ) {
322
- if( false === stream_copy_to_stream( $src, $dest, 1024 ) ) {
323
- $this->log( "Can not copy file; {$src} -> {$dest}" );
324
- fclose( $src );
325
- fclose( $dest );
326
- return false;
327
- }
328
- }
329
- }
330
- // Close any open handler
331
- fclose( $src );
332
- fclose( $dest );
333
- return true;
334
- }
335
-
336
- /**
337
- * Check if certain file is excluded from copying process
338
- *
339
- * @param string $file filename including ending without full path
340
- * @return boolean
341
- */
342
- private function isFileExcluded( $file ) {
343
-
344
- $excludedFiles = ( array ) $this->options->excludedFiles;
345
-
346
- $basenameFile = basename( $file );
347
-
348
-
349
- // Remove .htaccess and web.config from array excludedFiles if staging site is copied to a subdomain
350
- //if( $this->isCustomDirectory() ) {
351
- if( false === $this->isIdenticalHostname() ) {
352
- $excludedFiles = \array_diff( $excludedFiles, array("web.config", ".htaccess") );
353
- }
354
-
355
-
356
- // If file name exists
357
- if( in_array( $basenameFile, $excludedFiles ) ) {
358
- return true;
359
- }
360
-
361
- // Check for wildcards
362
- foreach ($excludedFiles as $pattern){
363
- if(fnmatch($pattern, $basenameFile)){
364
- return true;
365
- }
366
- }
367
-
368
- // Do not copy wp-config.php if the clone gets updated. This is for security purposes,
369
- // because if the updating process fails, the staging site would not be accessable any longer
370
- if( isset( $this->options->mainJob ) && $this->options->mainJob == "updating" && stripos( strrev( $file ), strrev( "wp-config.php" ) ) === 0 ) {
371
- return true;
372
- }
373
-
374
-
375
- return false;
376
- }
377
-
378
- /**
379
- * Check if custom target directory is used
380
- * @return boolean
381
- */
382
- // private function isCustomDirectory() {
383
- //
384
- // if( empty( $this->options->cloneDir ) ) {
385
- // return false;
386
- // }
387
- // return true;
388
- // }
389
-
390
- /**
391
- * Check if production and staging hostname are identical
392
- * If they are not identical we assume website is cloned to a subdomain and not into a subfolder
393
- * @return boolean
394
- */
395
- private function isIdenticalHostname() {
396
- // hostname of production site without scheme
397
- $siteurl = get_site_url();
398
- $url = parse_url( $siteurl );
399
- $productionHostname = $url['host'];
400
-
401
- // hostname of staging site without scheme
402
- $cloneUrl = empty( $this->options->cloneHostname ) ? $url : parse_url( $this->options->cloneHostname );
403
- $targetHostname = $cloneUrl['host'];
404
-
405
- // Check if target hostname beginns with the production hostname
406
- // Only compare the hostname without path
407
- if( wpstg_starts_with( $productionHostname, $targetHostname ) ) {
408
- return true;
409
- }
410
- return false;
411
- }
412
-
413
- /**
414
- * Check if certain file is excluded from copying process
415
- *
416
- * @param string $file filename including ending + (part) path e.g wp-content/db.php
417
- * @return boolean
418
- */
419
- private function isFileExcludedFullPath( $file ) {
420
- // If path + file exists
421
- foreach ( $this->options->excludedFilesFullPath as $excludedFile ) {
422
- if( false !== strpos( $file, $excludedFile ) ) {
423
- return true;
424
- }
425
- }
426
-
427
- return false;
428
- }
429
-
430
- /**
431
- * Replace backward slash with forward slash directory separator
432
- * Windows Compatibility Fix
433
- *
434
- * @param string $path Path
435
- * @return string
436
- */
437
- private function sanitizeDirectorySeparator( $path ) {
438
- return preg_replace( '/[\\\\]+/', '/', $path );
439
- }
440
-
441
- /**
442
- * Check if directory is excluded from copying
443
- * @param string $directory
444
- * @return bool
445
- */
446
- private function isDirectoryExcluded( $directory ) {
447
- // Make sure that wp-staging-pro directory / plugin is never excluded
448
- if( false !== strpos( $directory, 'wp-staging' ) || false !== strpos( $directory, 'wp-staging-pro' ) ) {
449
- return false;
450
- }
451
-
452
- $directory = trailingslashit( $this->sanitizeDirectorySeparator( $directory ) );
453
-
454
- foreach ( $this->options->excludedDirectories as $excludedDirectory ) {
455
- $excludedDirectory = trailingslashit( $this->sanitizeDirectorySeparator( $excludedDirectory ) );
456
- if( strpos( $directory, $excludedDirectory ) === 0 && !$this->isExtraDirectory( $directory ) ) {
457
- return true;
458
- }
459
- }
460
-
461
- return false;
462
- }
463
-
464
- /**
465
- * Check if directory is an extra directory and should be copied
466
- * @param string $directory
467
- * @return boolean
468
- */
469
- private function isExtraDirectory( $directory ) {
470
- $directory = $this->sanitizeDirectorySeparator( $directory );
471
-
472
- foreach ( $this->options->extraDirectories as $extraDirectory ) {
473
- if( strpos( $directory, $this->sanitizeDirectorySeparator( $extraDirectory ) ) === 0 ) {
474
- return true;
475
- }
476
- }
477
-
478
- return false;
479
- }
480
-
481
- }
1
+ <?php
2
+
3
+ namespace WPStaging\Backend\Modules\Jobs\Multisite;
4
+
5
+ use WPStaging\Backend\Modules\Jobs\JobExecutable;
6
+ // No Direct Access
7
+ use WPStaging\Utils\Logger;
8
+
9
+ if( !defined( "WPINC" ) ) {
10
+ die;
11
+ }
12
+
13
+ /**
14
+ * Class Files
15
+ * @package WPStaging\Backend\Modules\Jobs
16
+ */
17
+ class Files extends JobExecutable {
18
+
19
+ /**
20
+ * @var \SplFileObject
21
+ */
22
+ private $file;
23
+
24
+ /**
25
+ * @var int
26
+ */
27
+ private $maxFilesPerRun;
28
+
29
+ /**
30
+ * @var string
31
+ */
32
+ private $destination;
33
+
34
+ /**
35
+ * Initialization
36
+ */
37
+ public function initialize() {
38
+
39
+ $this->destination = $this->options->destinationDir;
40
+
41
+ $filePath = $this->cache->getCacheDir() . "files_to_copy." . $this->cache->getCacheExtension();
42
+
43
+ if( is_file( $filePath ) ) {
44
+ $this->file = new \SplFileObject( $filePath, 'r' );
45
+ }
46
+
47
+ // Informational logs
48
+ if( 0 == $this->options->currentStep ) {
49
+ $this->log( "Copying files..." );
50
+ }
51
+
52
+ $this->settings->batchSize = $this->settings->batchSize * 1000000;
53
+ $this->maxFilesPerRun = $this->settings->fileLimit;
54
+ //$this->maxFilesPerRun = ($this->settings->cpuLoad === 'low') ? 50 : 1;
55
+ }
56
+
57
+ /**
58
+ * Calculate Total Steps in This Job and Assign It to $this->options->totalSteps
59
+ * @return void
60
+ */
61
+ protected function calculateTotalSteps() {
62
+ $this->options->totalSteps = ceil( $this->options->totalFiles / $this->maxFilesPerRun );
63
+ }
64
+
65
+ /**
66
+ * Execute the Current Step
67
+ * Returns false when over threshold limits are hit or when the job is done, true otherwise
68
+ * @return bool
69
+ */
70
+ protected function execute() {
71
+ // Finished
72
+ if( $this->isFinished() ) {
73
+ $this->log( "Copying files finished" );
74
+ $this->prepareResponse( true, false );
75
+ return false;
76
+ }
77
+
78
+ // Get files and copy'em
79
+ if( !$this->getFilesAndCopy() ) {
80
+ $this->prepareResponse( false, false );
81
+ return false;
82
+ }
83
+
84
+ // Prepare and return response
85
+ $this->prepareResponse();
86
+
87
+ // Not finished
88
+ return true;
89
+ }
90
+
91
+ /**
92
+ * Get files and copy
93
+ * @return bool
94
+ */
95
+ private function getFilesAndCopy() {
96
+ // Over limits threshold
97
+ if( $this->isOverThreshold() ) {
98
+ // Prepare response and save current progress
99
+ $this->prepareResponse( false, false );
100
+ $this->saveOptions();
101
+ return false;
102
+ }
103
+
104
+ // Go to last copied line and than to next one
105
+ //if ($this->options->copiedFiles != 0) {
106
+ if( isset( $this->options->copiedFiles ) && $this->options->copiedFiles != 0 ) {
107
+ $this->file->seek( $this->options->copiedFiles - 1 );
108
+ }
109
+
110
+ $this->file->setFlags( \SplFileObject::SKIP_EMPTY | \SplFileObject::READ_AHEAD );
111
+
112
+ // Start time
113
+ //$start = microtime( true );
114
+ // Loop x files at a time
115
+ //$this->maxFilesPerRun = 300;
116
+ for ( $i = 0; $i < $this->maxFilesPerRun; $i++ ) {
117
+
118
+ // Reached timeout
119
+ // if( ( $timeout = apply_filters( 'wpstg_job_timeout', 10 ) ) ) {
120
+ // if( ( \microtime( true ) - $start ) > $timeout ) {
121
+ // // Prepare response and save current progress
122
+ // $this->prepareResponse( false, true );
123
+ // $this->saveOptions();
124
+ // return false;
125
+ // }
126
+ // }
127
+ // Increment copied files
128
+ // Do this anytime to make sure to not stuck in the same step / files
129
+ $this->options->copiedFiles++;
130
+
131
+ // End of file
132
+ if( $this->file->eof() ) {
133
+ break;
134
+ }
135
+
136
+ $file = $this->file->fgets();
137
+
138
+
139
+ $this->copyFile( $file );
140
+ }
141
+
142
+
143
+
144
+ $totalFiles = $this->options->copiedFiles;
145
+ // Log this only every 50 entries to keep the log small and to not block the rendering browser
146
+ if( $this->options->copiedFiles % 50 == 0 ) {
147
+ $this->log( "Total {$totalFiles} files processed" );
148
+ }
149
+
150
+ return true;
151
+ }
152
+
153
+ /**
154
+ * Checks Whether There is Any Job to Execute or Not
155
+ * @return bool
156
+ */
157
+ private function isFinished() {
158
+ return (
159
+ !isset( $this->options->isRunning ) ||
160
+ $this->options->currentStep > $this->options->totalSteps ||
161
+ $this->options->copiedFiles >= $this->options->totalFiles
162
+ );
163
+ }
164
+
165
+ /**
166
+ * @param string $file
167
+ * @return bool
168
+ */
169
+ private function copyFile( $file ) {
170
+
171
+ $file = trim( \WPStaging\WPStaging::getWPpath() . $file );
172
+
173
+ $file = wpstg_replace_windows_directory_separator($file);
174
+
175
+ $directory = dirname( $file );
176
+
177
+ // Directory is excluded
178
+ if( $this->isDirectoryExcluded( $directory ) ) {
179
+ $this->debugLog( "Skipping directory by rule: {$file}", Logger::TYPE_INFO );
180
+ return false;
181
+ }
182
+
183
+ // File is excluded
184
+ if( $this->isFileExcluded( $file ) ) {
185
+ $this->debugLog( "Skipping file by rule: {$file}", Logger::TYPE_INFO );
186
+ return false;
187
+ }
188
+ // Path + File is excluded
189
+ if( $this->isFileExcludedFullPath( $file ) ) {
190
+ $this->debugLog( "Skipping file by rule: {$file}", Logger::TYPE_INFO );
191
+ return false;
192
+ }
193
+
194
+ // Invalid file, skipping it as if succeeded
195
+ if( !is_file( $file ) ) {
196
+ $this->log( "File doesn't exist {$file}", Logger::TYPE_WARNING );
197
+ return true;
198
+ }
199
+ // Invalid file, skipping it as if succeeded
200
+ if( !is_readable( $file ) ) {
201
+ $this->log( "Can't read file {$file}", Logger::TYPE_WARNING );
202
+ return true;
203
+ }
204
+
205
+
206
+ // Get file size
207
+ $fileSize = filesize( $file );
208
+
209
+ // File is over maximum allowed file size (8MB)
210
+ if( $fileSize >= $this->settings->maxFileSize * 1000000 ) {
211
+ $this->log( "Skipping big file: {$file}", Logger::TYPE_INFO );
212
+ return false;
213
+ }
214
+
215
+ // Failed to get destination
216
+ if( false === ($destination = $this->getDestination( $file )) ) {
217
+ $this->log( "Can't get the destination of {$file}", Logger::TYPE_WARNING );
218
+ return false;
219
+ }
220
+
221
+ // File is over batch size
222
+ if( $fileSize >= $this->settings->batchSize ) {
223
+ $this->log( "Trying to copy big file: {$file} -> {$destination}", Logger::TYPE_INFO );
224
+ return $this->copyBig( $file, $destination, $this->settings->batchSize );
225
+ }
226
+
227
+ // Attempt to copy
228
+ if( !@copy( $file, $destination ) ) {
229
+ $errors = error_get_last();
230
+ $this->log( "Files: Failed to copy file to destination. Error: {$errors['message']} {$file} -> {$destination}", Logger::TYPE_ERROR );
231
+ return false;
232
+ }
233
+
234
+ // Set file permissions
235
+ @chmod( $destination, wpstg_get_permissions_for_file() );
236
+
237
+ $this->setDirPermissions( $destination );
238
+
239
+ return true;
240
+ }
241
+
242
+ /**
243
+ * Set directory permissions
244
+ * @param type $file
245
+ * @return boolean
246
+ */
247
+ private function setDirPermissions( $file ) {
248
+ $dir = dirname( $file );
249
+ if( is_dir( $dir ) ) {
250
+ @chmod( $dir, wpstg_get_permissions_for_directory() );
251
+ }
252
+ return false;
253
+ }
254
+
255
+ /**
256
+ * Gets destination file and checks if the directory exists, if it does not attempts to create it.
257
+ * If creating destination directory fails, it returns false, gives destination full path otherwise
258
+ * @param string $file
259
+ * @return bool|string
260
+ */
261
+ private function getDestination( $file ) {
262
+ //$file = $this->getMultisiteUploadFolder( $file );
263
+ $file = wpstg_replace_windows_directory_separator($file);
264
+ $rootPath = wpstg_replace_windows_directory_separator(\WPStaging\WPStaging::getWPpath());
265
+ $relativePath = str_replace( $rootPath, null, $file );
266
+ $destinationPath = $this->destination . $relativePath;
267
+ $destinationDirectory = dirname( $destinationPath );
268
+
269
+ if( !is_dir( $destinationDirectory ) && !@mkdir( $destinationDirectory, wpstg_get_permissions_for_directory(), true ) ) {
270
+ $this->log( "Files: Can not create directory {$destinationDirectory}", Logger::TYPE_ERROR );
271
+ return false;
272
+ }
273
+
274
+ return $this->sanitizeDirectorySeparator( $destinationPath );
275
+ }
276
+
277
+ /**
278
+ * Replace relative path of file if its located in multisite upload folder
279
+ * wp-content/uploads/sites/SITEID or old wordpress structure wp-content/blogs.dir/SITEID/files
280
+ * @return boolean
281
+ */
282
+ private function getMultisiteUploadFolder( $file ) {
283
+ // Check first which method is used
284
+ $uploads = wp_upload_dir();
285
+ $basedir = $uploads['basedir'];
286
+
287
+ if( false === strpos( $basedir, 'blogs.dir' ) ) {
288
+ // Since WP 3.5
289
+ $search = 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'sites' . DIRECTORY_SEPARATOR . get_current_blog_id();
290
+ $replace = 'wp-content' . DIRECTORY_SEPARATOR . 'uploads';
291
+ $uploadsFolder = str_replace( $search, $replace, $file );
292
+ } else {
293
+ // old blog structure
294
+ $search = 'wp-content' . DIRECTORY_SEPARATOR . 'blogs.dir' . DIRECTORY_SEPARATOR . get_current_blog_id() . DIRECTORY_SEPARATOR . 'files';
295
+ $replace = 'wp-content' . DIRECTORY_SEPARATOR . 'uploads';
296
+ $uploadsFolder = str_replace( $search, $replace, $file );
297
+ }
298
+
299
+ return $uploadsFolder;
300
+ }
301
+
302
+ /**
303
+ * Copy bigger files than $this->settings->batchSize
304
+ * @param string $src
305
+ * @param string $dst
306
+ * @param int $buffersize
307
+ * @return boolean
308
+ */
309
+ private function copyBig( $src, $dst, $buffersize ) {
310
+ $src = fopen( $src, 'r' );
311
+ $dest = fopen( $dst, 'w' );
312
+
313
+ // Try first method:
314
+ while ( !feof( $src ) ) {
315
+ if( false === fwrite( $dest, fread( $src, $buffersize ) ) ) {
316
+ $error = true;
317
+ }
318
+ }
319
+ // Try second method if first one failed
320
+ if( isset( $error ) && ($error === true) ) {
321
+ while ( !feof( $src ) ) {
322
+ if( false === stream_copy_to_stream( $src, $dest, 1024 ) ) {
323
+ $this->log( "Can not copy file; {$src} -> {$dest}" );
324
+ fclose( $src );
325
+ fclose( $dest );
326
+ return false;
327
+ }
328
+ }
329
+ }
330
+ // Close any open handler
331
+ fclose( $src );
332
+ fclose( $dest );
333
+ return true;
334
+ }
335
+
336
+ /**
337
+ * Check if certain file is excluded from copying process
338
+ *
339
+ * @param string $file filename including ending without full path
340
+ * @return boolean
341
+ */
342
+ private function isFileExcluded( $file ) {
343
+
344
+ $excludedFiles = ( array ) $this->options->excludedFiles;
345
+
346
+ $basenameFile = basename( $file );
347
+
348
+
349
+ // Remove .htaccess and web.config from array excludedFiles if staging site is copied to a subdomain
350
+ //if( $this->isCustomDirectory() ) {
351
+ if( false === $this->isIdenticalHostname() ) {
352
+ $excludedFiles = \array_diff( $excludedFiles, array("web.config", ".htaccess") );
353
+ }
354
+
355
+
356
+ // If file name exists
357
+ if( in_array( $basenameFile, $excludedFiles ) ) {
358
+ return true;
359
+ }
360
+
361
+ // Check for wildcards
362
+ foreach ($excludedFiles as $pattern){
363
+ if(fnmatch($pattern, $basenameFile)){
364
+ return true;
365
+ }
366
+ }
367
+
368
+ // Do not copy wp-config.php if the clone gets updated. This is for security purposes,
369
+ // because if the updating process fails, the staging site would not be accessable any longer
370
+ if( isset( $this->options->mainJob ) && $this->options->mainJob == "updating" && stripos( strrev( $file ), strrev( "wp-config.php" ) ) === 0 ) {
371
+ return true;
372
+ }
373
+
374
+
375
+ return false;
376
+ }
377
+
378
+ /**
379
+ * Check if custom target directory is used
380
+ * @return boolean
381
+ */
382
+ // private function isCustomDirectory() {
383
+ //
384
+ // if( empty( $this->options->cloneDir ) ) {
385
+ // return false;
386
+ // }
387
+ // return true;
388
+ // }
389
+
390
+ /**
391
+ * Check if production and staging hostname are identical
392
+ * If they are not identical we assume website is cloned to a subdomain and not into a subfolder
393
+ * @return boolean
394
+ */
395
+ private function isIdenticalHostname() {
396
+ // hostname of production site without scheme
397
+ $siteurl = get_site_url();
398
+ $url = parse_url( $siteurl );
399
+ $productionHostname = $url['host'];
400
+
401
+ // hostname of staging site without scheme
402
+ $cloneUrl = empty( $this->options->cloneHostname ) ? $url : parse_url( $this->options->cloneHostname );
403
+ $targetHostname = $cloneUrl['host'];
404
+
405
+ // Check if target hostname beginns with the production hostname
406
+ // Only compare the hostname without path
407
+ if( wpstg_starts_with( $productionHostname, $targetHostname ) ) {
408
+ return true;
409
+ }
410
+ return false;
411
+ }
412
+
413
+ /**
414
+ * Check if certain file is excluded from copying process
415
+ *
416
+ * @param string $file filename including ending + (part) path e.g wp-content/db.php
417
+ * @return boolean
418
+ */
419
+ private function isFileExcludedFullPath( $file ) {
420
+ // If path + file exists
421
+ foreach ( $this->options->excludedFilesFullPath as $excludedFile ) {
422
+ if( false !== strpos( $file, $excludedFile ) ) {
423
+ return true;
424
+ }
425
+ }
426
+
427
+ return false;
428
+ }
429
+
430
+ /**
431
+ * Replace backward slash with forward slash directory separator
432
+ * Windows Compatibility Fix
433
+ *
434
+ * @param string $path Path
435
+ * @return string
436
+ */
437
+ private function sanitizeDirectorySeparator( $path ) {
438
+ return preg_replace( '/[\\\\]+/', '/', $path );
439
+ }
440
+
441
+ /**
442
+ * Check if directory is excluded from copying
443
+ * @param string $directory
444
+ * @return bool
445
+ */
446
+ private function isDirectoryExcluded( $directory ) {
447
+ // Make sure that wp-staging-pro directory / plugin is never excluded
448
+ if( false !== strpos( $directory, 'wp-staging' ) || false !== strpos( $directory, 'wp-staging-pro' ) ) {
449
+ return false;
450
+ }
451
+
452
+ $directory = trailingslashit( $this->sanitizeDirectorySeparator( $directory ) );
453
+
454
+ foreach ( $this->options->excludedDirectories as $excludedDirectory ) {
455
+ $excludedDirectory = trailingslashit( $this->sanitizeDirectorySeparator( $excludedDirectory ) );
456
+ if( strpos( $directory, $excludedDirectory ) === 0 && !$this->isExtraDirectory( $directory ) ) {
457
+ return true;
458
+ }
459
+ }
460
+
461
+ return false;
462
+ }
463
+
464
+ /**
465
+ * Check if directory is an extra directory and should be copied
466
+ * @param string $directory
467
+ * @return boolean
468
+ */
469
+ private function isExtraDirectory( $directory ) {
470
+ $directory = $this->sanitizeDirectorySeparator( $directory );
471
+
472
+ foreach ( $this->options->extraDirectories as $extraDirectory ) {
473
+ if( strpos( $directory, $this->sanitizeDirectorySeparator( $extraDirectory ) ) === 0 ) {
474
+ return true;
475
+ }
476
+ }
477
+
478
+ return false;
479
+ }
480
+
481
+ }
apps/Backend/Modules/Jobs/Multisite/Finish.php CHANGED
@@ -1,141 +1,141 @@
1
- <?php
2
-
3
- namespace WPStaging\Backend\Modules\Jobs\Multisite;
4
-
5
- use WPStaging\WPStaging;
6
- use WPStaging\Backend\Modules\Jobs\Job;
7
- use WPStaging\Utils\Multisite;
8
-
9
- /**
10
- * Class Finish
11
- * @package WPStaging\Backend\Modules\Jobs
12
- */
13
- class Finish extends Job {
14
-
15
- /**
16
- * Clone Key
17
- * @var string
18
- */
19
- private $clone = '';
20
-
21
- /**
22
- * Start Module
23
- * @return object
24
- */
25
- public function start() {
26
- // sanitize the clone name before saving
27
- $this->clone = preg_replace( "#\W+#", '-', strtolower( $this->options->clone ) );
28
-
29
- // Delete Cache Files
30
- $this->deleteCacheFiles();
31
-
32
- // Prepare clone records & save scanned directories for delete job later
33
- $this->prepareCloneDataRecords();
34
-
35
- $this->options->isRunning = false;
36
-
37
- $multisite = new Multisite;
38
-
39
-
40
-
41
- $return = array(
42
- "directoryName" => $this->options->cloneDirectoryName,
43
- "path" => trailingslashit( $this->options->destinationDir ),
44
- "url" => $this->getDestinationUrl(),
45
- "number" => $this->options->cloneNumber,
46
- "version" => \WPStaging\WPStaging::VERSION,
47
- "status" => 'finished',
48
- "prefix" => $this->options->prefix,
49
- "last_msg" => $this->logger->getLastLogMsg(),
50
- "job" => $this->options->currentJob,
51
- "percentage" => 100
52
- );
53
-
54
- //$this->flush();
55
- do_action( 'wpstg_cloning_complete', $this->options );
56
-
57
-
58
- return ( object ) $return;
59
- }
60
-
61
- /**
62
- * Delete Cache Files
63
- */
64
- protected function deleteCacheFiles() {
65
- $this->log( "Finish: Deleting clone job's cache files..." );
66
-
67
- // Clean cache files
68
- $this->cache->delete( "clone_options" );
69
- $this->cache->delete( "files_to_copy" );
70
-
71
- $this->log( "Finish: Clone job's cache files have been deleted!" );
72
- }
73
-
74
- /**
75
- * Prepare clone records
76
- * @return bool
77
- */
78
- protected function prepareCloneDataRecords() {
79
- // Check if clones still exist
80
- $this->log( "Finish: Verifying existing clones..." );
81
-
82
- // Clone data already exists
83
- if( isset( $this->options->existingClones[$this->options->clone] ) ) {
84
- $this->options->existingClones[$this->options->clone]['datetime'] = time();
85
- $this->options->existingClones[$this->options->clone]['url'] = $this->getDestinationUrl();
86
- $this->options->existingClones[$this->options->clone]['status'] = 'finished';
87
- $this->options->existingClones[$this->options->clone]['prefix'] = $this->options->prefix;
88
- update_option( "wpstg_existing_clones_beta", $this->options->existingClones );
89
- $this->log( "Finish: The job finished!" );
90
- return true;
91
- }
92
-
93
- // Save new clone data
94
- $this->log( "Finish: {$this->options->clone}'s clone job's data is not in database, generating data" );
95
-
96
- // sanitize the clone name before saving
97
- //$clone = preg_replace("#\W+#", '-', strtolower($this->options->clone));
98
-
99
- $this->options->existingClones[$this->clone] = array(
100
- "directoryName" => $this->options->cloneDirectoryName,
101
- "path" => trailingslashit( $this->options->destinationDir ),
102
- "url" => $this->getDestinationUrl(),
103
- "number" => $this->options->cloneNumber,
104
- "version" => \WPStaging\WPStaging::VERSION,
105
- "status" => "finished",
106
- "prefix" => $this->options->prefix,
107
- "datetime" => time(),
108
- "databaseUser" => $this->options->databaseUser,
109
- "databasePassword" => $this->options->databasePassword,
110
- "databaseDatabase" => $this->options->databaseDatabase,
111
- "databaseServer" => $this->options->databaseServer,
112
- "databasePrefix" => $this->options->databasePrefix,
113
- );
114
-
115
- if( false === update_option( "wpstg_existing_clones_beta", $this->options->existingClones ) ) {
116
- $this->log( "Finish: Failed to save {$this->options->clone}'s clone job data to database'" );
117
- return false;
118
- }
119
-
120
- return true;
121
- }
122
-
123
- /**
124
- * Get destination Hostname depending on wheather WP has been installed in sub dir or not
125
- * @return type
126
- */
127
- private function getDestinationUrl() {
128
-
129
- if( !empty( $this->options->cloneHostname ) ) {
130
- return $this->options->cloneHostname;
131
- }
132
-
133
- //return trailingslashit( $this->multisiteHomeDomain ) . $this->options->cloneDirectoryName;
134
- // Get the path to the main multisite without appending and trailingslash e.g. wordpress
135
- $multisitePath = defined( 'PATH_CURRENT_SITE' ) ? PATH_CURRENT_SITE : '/';
136
- return rtrim( $this->multisiteHomeDomain, '/\\' ) . $multisitePath . $this->options->cloneDirectoryName;
137
- //$multisitePath = defined( 'PATH_CURRENT_SITE' ) ? str_replace( '/', '', PATH_CURRENT_SITE ) : '';
138
- //return trailingslashit( $this->multisiteHomeDomain ) . $multisitePath . '/' . $this->options->cloneDirectoryName;
139
- }
140
-
141
- }
1
+ <?php
2
+
3
+ namespace WPStaging\Backend\Modules\Jobs\Multisite;
4
+
5
+ use WPStaging\WPStaging;
6
+ use WPStaging\Backend\Modules\Jobs\Job;
7
+ use WPStaging\Utils\Multisite;
8
+
9
+ /**
10
+ * Class Finish
11
+ * @package WPStaging\Backend\Modules\Jobs
12
+ */
13
+ class Finish extends Job {
14
+
15
+ /**
16
+ * Clone Key
17
+ * @var string
18
+ */
19
+ private $clone = '';
20
+
21
+ /**
22
+ * Start Module
23
+ * @return object
24
+ */
25
+ public function start() {
26
+ // sanitize the clone name before saving
27
+ $this->clone = preg_replace( "#\W+#", '-', strtolower( $this->options->clone ) );
28
+
29
+ // Delete Cache Files
30
+ $this->deleteCacheFiles();
31
+
32
+ // Prepare clone records & save scanned directories for delete job later
33
+ $this->prepareCloneDataRecords();
34
+
35
+ $this->options->isRunning = false;
36
+
37
+ $multisite = new Multisite;
38
+
39
+
40
+
41
+ $return = array(
42
+ "directoryName" => $this->options->cloneDirectoryName,
43
+ "path" => trailingslashit( $this->options->destinationDir ),
44
+ "url" => $this->getDestinationUrl(),
45
+ "number" => $this->options->cloneNumber,
46
+ "version" => \WPStaging\WPStaging::VERSION,
47
+ "status" => 'finished',
48
+ "prefix" => $this->options->prefix,
49
+ "last_msg" => $this->logger->getLastLogMsg(),
50
+ "job" => $this->options->currentJob,
51
+ "percentage" => 100
52
+ );
53
+
54
+ //$this->flush();
55
+ do_action( 'wpstg_cloning_complete', $this->options );
56
+
57
+
58
+ return ( object ) $return;
59
+ }
60
+
61
+ /**
62
+ * Delete Cache Files
63
+ */
64
+ protected function deleteCacheFiles() {
65
+ $this->log( "Finish: Deleting clone job's cache files..." );
66
+
67
+ // Clean cache files
68
+ $this->cache->delete( "clone_options" );
69
+ $this->cache->delete( "files_to_copy" );
70
+
71
+ $this->log( "Finish: Clone job's cache files have been deleted!" );
72
+ }
73
+
74
+ /**
75
+ * Prepare clone records
76
+ * @return bool
77
+ */
78
+ protected function prepareCloneDataRecords() {
79
+ // Check if clones still exist
80
+ $this->log( "Finish: Verifying existing clones..." );
81
+
82
+ // Clone data already exists
83
+ if( isset( $this->options->existingClones[$this->options->clone] ) ) {
84
+ $this->options->existingClones[$this->options->clone]['datetime'] = time();
85
+ $this->options->existingClones[$this->options->clone]['url'] = $this->getDestinationUrl();
86
+ $this->options->existingClones[$this->options->clone]['status'] = 'finished';
87
+ $this->options->existingClones[$this->options->clone]['prefix'] = $this->options->prefix;
88
+ update_option( "wpstg_existing_clones_beta", $this->options->existingClones );
89
+ $this->log( "Finish: The job finished!" );
90
+ return true;
91
+ }
92
+
93
+ // Save new clone data
94
+ $this->log( "Finish: {$this->options->clone}'s clone job's data is not in database, generating data" );
95
+
96
+ // sanitize the clone name before saving
97
+ //$clone = preg_replace("#\W+#", '-', strtolower($this->options->clone));
98
+
99
+ $this->options->existingClones[$this->clone] = array(
100
+ "directoryName" => $this->options->cloneDirectoryName,
101
+ "path" => trailingslashit( $this->options->destinationDir ),
102
+ "url" => $this->getDestinationUrl(),
103
+ "number" => $this->options->cloneNumber,
104
+ "version" => \WPStaging\WPStaging::VERSION,
105
+ "status" => "finished",
106
+ "prefix" => $this->options->prefix,
107
+ "datetime" => time(),
108
+ "databaseUser" => $this->options->databaseUser,
109
+ "databasePassword" => $this->options->databasePassword,
110
+ "databaseDatabase" => $this->options->databaseDatabase,
111
+ "databaseServer" => $this->options->databaseServer,
112
+ "databasePrefix" => $this->options->databasePrefix,
113
+ );
114
+
115
+ if( false === update_option( "wpstg_existing_clones_beta", $this->options->existingClones ) ) {
116
+ $this->log( "Finish: Failed to save {$this->options->clone}'s clone job data to database'" );
117
+ return false;
118
+ }
119
+
120
+ return true;
121
+ }
122
+
123
+ /**
124
+ * Get destination Hostname depending on wheather WP has been installed in sub dir or not
125
+ * @return type
126
+ */
127
+ private function getDestinationUrl() {
128
+
129
+ if( !empty( $this->options->cloneHostname ) ) {
130
+ return $this->options->cloneHostname;
131
+ }
132
+
133
+ //return trailingslashit( $this->multisiteHomeDomain ) . $this->options->cloneDirectoryName;
134
+ // Get the path to the main multisite without appending and trailingslash e.g. wordpress
135
+ $multisitePath = defined( 'PATH_CURRENT_SITE' ) ? PATH_CURRENT_SITE : '/';
136
+ return rtrim( $this->multisiteHomeDomain, '/\\' ) . $multisitePath . $this->options->cloneDirectoryName;
137
+ //$multisitePath = defined( 'PATH_CURRENT_SITE' ) ? str_replace( '/', '', PATH_CURRENT_SITE ) : '';
138
+ //return trailingslashit( $this->multisiteHomeDomain ) . $multisitePath . '/' . $this->options->cloneDirectoryName;
139
+ }
140
+
141
+ }
apps/Backend/Modules/Jobs/Multisite/SearchReplace.php CHANGED
@@ -1,839 +1,839 @@
1
- <?php
2
-
3
- namespace WPStaging\Backend\Modules\Jobs\Multisite;
4
-
5
- // No Direct Access
6
- if( !defined( "WPINC" ) ) {
7
- die;
8
- }
9
-
10
- use WPStaging\WPStaging;
11
- use WPStaging\Utils\Strings;
12
- use WPStaging\Utils\Helper;
13
- use WPStaging\Utils\Multisite;
14
- use WPStaging\Backend\Modules\Jobs\JobExecutable;
15
-
16
- /**
17
- * Class Database
18
- * @package WPStaging\Backend\Modules\Jobs
19
- */
20
- class SearchReplace extends JobExecutable {
21
-
22
- /**
23
- * @var int
24
- */
25
- private $total = 0;
26
-
27
- /**
28
- * @var \WPDB
29
- */
30
- public $db;
31
-
32
- /**
33
- *
34
- * @var Obj
35
- */
36
- private $strings;
37
-
38
- /**
39
- *
40
- * @var string
41
- */
42
- private $destinationHostname;
43
-
44
- /**
45
- *
46
- * @var string
47
- */
48
- private $sourceHostname;
49
-
50
- /**
51
- *
52
- * @var string
53
- */
54
- //private $targetDir;
55
-
56
- /**
57
- * The prefix of the new database tables which are used for the live site after updating tables
58
- * @var string
59
- */
60
- public $tmpPrefix;
61
-
62
- /**
63
- * Initialize
64
- */
65
- public function initialize() {
66
- $this->total = count( $this->options->tables );
67
- $this->db = WPStaging::getInstance()->get( "wpdb" );
68
- $this->tmpPrefix = $this->options->prefix;
69
- $this->strings = new Strings();
70
- $this->sourceHostname = $this->getSourceHostname();
71
- $this->destinationHostname = $this->getDestinationHostname();
72
- }
73
-
74
- public function start() {
75
- // Skip job. Nothing to do
76
- if( $this->options->totalSteps === 0 ) {
77
- $this->prepareResponse( true, false );
78
- }
79
-
80
- $this->run();
81
-
82
- // Save option, progress
83
- $this->saveOptions();
84
-
85
- return ( object ) $this->response;
86
- }
87
-
88
- /**
89
- * Calculate Total Steps in This Job and Assign It to $this->options->totalSteps
90
- * @return void
91
- */
92
- protected function calculateTotalSteps() {
93
- $this->options->totalSteps = $this->total;
94
- }
95
-
96
- /**
97
- * Execute the Current Step
98
- * Returns false when over threshold limits are hit or when the job is done, true otherwise
99
- * @return bool
100
- */
101
- protected function execute() {
102
- // Over limits threshold
103
- if( $this->isOverThreshold() ) {
104
- // Prepare response and save current progress
105
- $this->prepareResponse( false, false );
106
- $this->saveOptions();
107
- return false;
108
- }
109
-
110
- // No more steps, finished
111
- if( $this->options->currentStep > $this->total || !isset( $this->options->tables[$this->options->currentStep] ) ) {
112
- $this->prepareResponse( true, false );
113
- return false;
114
- }
115
-
116
- // Table is excluded
117
- if( in_array( $this->options->tables[$this->options->currentStep], $this->options->excludedTables ) ) {
118
- $this->prepareResponse();
119
- return true;
120
- }
121
-
122
- // Search & Replace
123
- if( !$this->stopExecution() && !$this->updateTable( $this->options->tables[$this->options->currentStep] ) ) {
124
- // Prepare Response
125
- $this->prepareResponse( false, false );
126
-
127
- // Not finished
128
- return true;
129
- }
130
-
131
-
132
- // Prepare Response
133
- $this->prepareResponse();
134
-
135
- // Not finished
136
- return true;
137
- }
138
-
139
- /**
140
- * Stop Execution immediately
141
- * return mixed bool | json
142
- */
143
- private function stopExecution() {
144
- if( $this->db->prefix == $this->tmpPrefix ) {
145
- $this->returnException( 'Fatal Error 9: Prefix ' . $this->db->prefix . ' is used for the live site hence it can not be used for the staging site as well. Please ask support@wp-staging.com how to resolve this.' );
146
- }
147
- return false;
148
- }
149
-
150
- /**
151
- * Copy Tables
152
- * @param string $tableName
153
- * @return bool
154
- */
155
- private function updateTable( $tableName ) {
156
- $strings = new Strings();
157
- $table = $strings->str_replace_first( $this->db->prefix, '', $tableName );
158
- $newTableName = $this->tmpPrefix . $table;
159
-
160
- // Save current job
161
- $this->setJob( $newTableName );
162
-
163
- // Beginning of the job
164
- if( !$this->startJob( $newTableName, $tableName ) ) {
165
- return true;
166
- }
167
- // Copy data
168
- $this->startReplace( $newTableName );
169
-
170
- // Finish the step
171
- return $this->finishStep();
172
- }
173
-
174
- /**
175
- * Get source Hostname depending on wheather WP has been installed in sub dir or not
176
- * @return type
177
- */
178
- public function getSourceHostname() {
179
-
180
- if( $this->isSubDir() ) {
181
- return trailingslashit( $this->multisiteHomeUrlWithoutScheme ) . '/' . $this->getSubDir();
182
- }
183
- return $this->multisiteHomeUrlWithoutScheme;
184
- }
185
-
186
- /**
187
- * Get destination Hostname depending on wheather WP has been installed in sub dir or not
188
- * @return type
189
- */
190
- public function getDestinationHostname() {
191
-
192
- if( !empty( $this->options->cloneHostname ) ) {
193
- return $this->strings->getUrlWithoutScheme( $this->options->cloneHostname );
194
- }
195
-
196
- if( $this->isSubDir() ) {
197
- return trailingslashit( $this->strings->getUrlWithoutScheme( $this->multisiteDomainWithoutScheme ) ) . $this->getSubDir() . '/' . $this->options->cloneDirectoryName;
198
- }
199
-
200
- // Get the path to the main multisite without appending and trailingslash e.g. wordpress
201
- $multisitePath = defined( 'PATH_CURRENT_SITE' ) ? PATH_CURRENT_SITE : '/';
202
- $url = rtrim( $this->strings->getUrlWithoutScheme( $this->multisiteDomainWithoutScheme ), '/\\' ) . $multisitePath . $this->options->cloneDirectoryName;
203
- //$multisitePath = defined( 'PATH_CURRENT_SITE' ) ? str_replace( '/', '', PATH_CURRENT_SITE ) : '';
204
- //$url = trailingslashit( $this->strings->getUrlWithoutScheme( $this->multisiteDomainWithoutScheme ) ) . $multisitePath . '/' . $this->options->cloneDirectoryName;
205
- return $url;
206
- }
207
-
208
- /**
209
- * Get the install sub directory if WP is installed in sub directory
210
- * @return string
211
- */
212
- private function getSubDir() {
213
- $home = get_option( 'home' );
214
- $siteurl = get_option( 'siteurl' );
215
-
216
- if( empty( $home ) || empty( $siteurl ) ) {
217
- return '';
218
- }
219
-
220
- $dir = str_replace( $home, '', $siteurl );
221
- return str_replace( '/', '', $dir );
222
- }
223
-
224
- /**
225
- * Start search replace job
226
- * @param string $new
227
- * @param string $old
228
- */
229
- private function startReplace( $table ) {
230
- $rows = $this->options->job->start + $this->settings->querySRLimit;
231
- $this->log(
232
- "DB Search & Replace: Table {$table} {$this->options->job->start} to {$rows} records"
233
- );
234
-
235
- // Search & Replace
236
- $this->searchReplace( $table, $rows, array() );
237
-
238
- // Set new offset
239
- $this->options->job->start += $this->settings->querySRLimit;
240
- }
241
-
242
- /**
243
- * Returns the number of pages in a table.
244
- * @access public
245
- * @return int
246
- */
247
- // private function get_pages_in_table( $table ) {
248
- //
249
- // // Table does not exist
250
- // $result = $this->db->query( "SHOW TABLES LIKE '{$table}'" );
251
- // if( !$result || 0 === $result ) {
252
- // return 0;
253
- // }
254
- //
255
- // $table = esc_sql( $table );
256
- // $rows = $this->db->get_var( "SELECT COUNT(*) FROM $table" );
257
- // $pages = ceil( $rows / $this->settings->querySRLimit );
258
- // return absint( $pages );
259
- // }
260
-
261
- /**
262
- * Gets the columns in a table.
263
- * @access public
264
- * @param string $table The table to check.
265
- * @return array
266
- */
267
- private function get_columns( $table ) {
268
- $primary_key = null;
269
- $columns = array();
270
- $fields = $this->db->get_results( 'DESCRIBE ' . $table );
271
- if( is_array( $fields ) ) {
272
- foreach ( $fields as $column ) {
273
- $columns[] = $column->Field;
274
- if( $column->Key == 'PRI' ) {
275
- $primary_key = $column->Field;
276
- }
277
- }
278
- }
279
- return array($primary_key, $columns);
280
- }
281
-
282
- /**
283
- * Return absolute destination path
284
- * @return string
285
- */
286
- private function getAbsDestination() {
287
- if( empty( $this->options->cloneDir ) ) {
288
- return \WPStaging\WPStaging::getWPpath();
289
- }
290
- return trailingslashit( $this->options->cloneDir );
291
- }
292
-
293
- /**
294
- * Adapated from interconnect/it's search/replace script, adapted from Better Search Replace
295
- *
296
- * Modified to use WordPress wpdb functions instead of PHP's native mysql/pdo functions,
297
- * and to be compatible with batch processing.
298
- *
299
- * @link https://interconnectit.com/products/search-and-replace-for-wordpress-databases/
300
- *
301
- * @access public
302
- * @param string $table The table to run the replacement on.
303
- * @param int $page The page/block to begin the query on.
304
- * @param array $args An associative array containing arguments for this run.
305
- * @return array
306
- */
307
- private function searchReplace( $table, $page, $args ) {
308
-
309
- if( $this->thirdParty->isSearchReplaceExcluded( $table ) ) {
310
- $this->log( "DB Search & Replace: Skip {$table}", \WPStaging\Utils\Logger::TYPE_INFO );
311
- return true;
312
- }
313
-
314
- // Load up the default settings for this chunk.
315
- $table = esc_sql( $table );
316
- //$current_page = $this->options->job->start + $this->settings->querySRLimit;
317
- //$pages = $this->get_pages_in_table( $table );
318
- // Search URL example.com/staging and root path to staging site /var/www/htdocs/staging
319
- $args['search_for'] = array(
320
- '//' . $this->getSourceHostname(),
321
- ABSPATH,
322
- '\/\/' . str_replace( '/', '\/', $this->getSourceHostname() ), // Used by revslider and several visual editors
323
- '%2F%2F' . str_replace( '/', '%2F', $this->getSourceHostname() ), // HTML entitity for WP Backery Page Builder Plugin
324
- //$this->getImagePathLive()
325
- );
326
-
327
-
328
- $args['replace_with'] = array(
329
- '//' . $this->getDestinationHostname(),
330
- $this->options->destinationDir,
331
- '\/\/' . str_replace( '/', '\/', $this->getDestinationHostname() ), // Used by revslider and several visual editors
332
- '%2F%2F' . str_replace( '/', '%2F', $this->getDestinationHostname() ), // HTML entitity for WP Backery Page Builder Plugin
333
- //$this->getImagePathStaging()
334
- );
335
-
336
- $this->debugLog( "DB Search & Replace: Search: {$args['search_for'][0]}", \WPStaging\Utils\Logger::TYPE_INFO );
337
- $this->debugLog( "DB Search & Replace: Replace: {$args['replace_with'][0]}", \WPStaging\Utils\Logger::TYPE_INFO );
338
-
339
-
340
-
341
- $args['replace_guids'] = 'off';
342
- $args['dry_run'] = 'off';
343
- $args['case_insensitive'] = false;
344
- //$args['replace_mails'] = 'off';
345
- $args['skip_transients'] = 'on';
346
-
347
-
348
- // Allow filtering of search & replace parameters
349
- $args = apply_filters( 'wpstg_clone_searchreplace_params', $args );
350
-
351
- // Get a list of columns in this table.
352
- list( $primary_key, $columns ) = $this->get_columns( $table );
353
-
354
- // Bail out early if there isn't a primary key.
355
- // We commented this to search & replace through tables which have no primary keys like wp_revslider_slides
356
- // @todo test this carefully. If it causes (performance) issues we need to activate it again!
357
- // @since 2.4.4
358
- // if( null === $primary_key ) {
359
- // return false;
360
- // }
361
-
362
- $current_row = 0;
363
- $start = $this->options->job->start;
364
- $end = $this->settings->querySRLimit;
365
-
366
- // Grab the content of the table.
367
- $data = $this->db->get_results( "SELECT * FROM $table LIMIT $start, $end", ARRAY_A );
368
-
369
- // Filter certain rows option_name in wpstg_options
370
- $filter = array(
371
- 'Admin_custome_login_Slidshow',
372
- 'Admin_custome_login_Social',
373
- 'Admin_custome_login_logo',
374
- 'Admin_custome_login_text',
375
- 'Admin_custome_login_login',
376
- 'Admin_custome_login_top',
377
- 'Admin_custome_login_dashboard',
378
- 'Admin_custome_login_Version',
379
- 'upload_path'
380
- );
381
-
382
- $filter = apply_filters( 'wpstg_clone_searchreplace_excl_rows', $filter );
383
-
384
- // Loop through the data.
385
- foreach ( $data as $row ) {
386
- $current_row++;
387
- $update_sql = array();
388
- $where_sql = array();
389
- $upd = false;
390
-
391
- // Skip rows below
392
- if( isset( $row['option_name'] ) && in_array( $row['option_name'], $filter ) ) {
393
- continue;
394
- }
395
-
396
- // Skip rows with transients (They can store huge data and we need to save memory)
397
- if( isset( $row['option_name'] ) && 'on' === $args['skip_transients'] && false !== strpos( $row['option_name'], '_transient' ) ) {
398
- continue;
399
- }
400
- // Skip rows with more than 5MB to save memory
401
- if( isset( $row['option_value'] ) && strlen( $row['option_value'] ) >= 5000000 ) {
402
- continue;
403
- }
404
-
405
-
406
- foreach ( $columns as $column ) {
407
-
408
- $dataRow = $row[$column];
409
-
410
- // Skip rows larger than 5MB
411
- $size = strlen( $dataRow );
412
- if( $size >= 5000000 ) {
413
- continue;
414
- }
415
-
416
- // Skip Primary key
417
- if( $column == $primary_key ) {
418
- $where_sql[] = $column . ' = "' . $this->mysql_escape_mimic( $dataRow ) . '"';
419
- continue;
420
- }
421
-
422
- // Skip GUIDs by default.
423
- if( 'on' !== $args['replace_guids'] && 'guid' == $column ) {
424
- continue;
425
- }
426
-
427
- // Skip mail addresses
428
- // if( 'off' === $args['replace_mails'] && false !== strpos( $dataRow, '@' . $this->multisiteDomainWithoutScheme ) ) {
429
- // continue;
430
- // }
431
- // Check options table
432
- if( $this->options->prefix . 'options' === $table ) {
433
-
434
- // Skip certain options
435
- // if( isset( $should_skip ) && true === $should_skip ) {
436
- // $should_skip = false;
437
- // continue;
438
- // }
439
- // Skip this row
440
- if( 'wpstg_existing_clones_beta' === $dataRow ||
441
- 'wpstg_existing_clones' === $dataRow ||
442
- 'wpstg_settings' === $dataRow ||
443
- 'wpstg_license_status' === $dataRow ||
444
- 'siteurl' === $dataRow ||
445
- 'home' === $dataRow
446
- ) {
447
- //$should_skip = true;
448
- continue;
449
- }
450
- }
451
-
452
- // Check the path delimiter for / or \/ and remove one of those which prevents from resulting in wrong syntax like domain.com/staging\/.
453
- // 1. local.wordpress.test -> local.wordpress.test/staging
454
- // 2. local.wordpress.test\/ -> local.wordpress.test\/staging\/
455
- $tmp = $args;
456
- if( false === strpos( $dataRow, $tmp['search_for'][0] ) ) {
457
- array_shift( $tmp['search_for'] ); // rtrim( $this->homeUrl, '/' ),
458
- array_shift( $tmp['replace_with'] ); // rtrim( $this->homeUrl, '/' ) . '/' . $this->options->cloneDirectoryName,
459
- } else {
460
- unset( $tmp['search_for'][1] );
461
- unset( $tmp['replace_with'][1] );
462
- // recount array
463
- $tmp['search_for'] = array_values( $tmp['search_for'] );
464
- $tmp['replace_with'] = array_values( $tmp['replace_with'] );
465
- }
466
-
467
- // Run a search replace on the data row and respect the serialisation.
468
- $i = 0;
469
- foreach ( $tmp['search_for'] as $replace ) {
470
- $dataRow = $this->recursive_unserialize_replace( $tmp['search_for'][$i], $tmp['replace_with'][$i], $dataRow, false, $args['case_insensitive'] );
471
- $i++;
472
- }
473
- unset( $replace );
474
- unset( $i );
475
- unset( $tmp );
476
-
477
- // Something was changed
478
- if( $row[$column] != $dataRow ) {
479
- $update_sql[] = $column . ' = "' . $this->mysql_escape_mimic( $dataRow ) . '"';
480
- $upd = true;
481
- }
482
- }
483
-
484
- // Determine what to do with updates.
485
- if( $args['dry_run'] === 'on' ) {
486
- // Don't do anything if a dry run
487
- } elseif( $upd && !empty( $where_sql ) ) {
488
- // If there are changes to make, run the query.
489
- $sql = 'UPDATE ' . $table . ' SET ' . implode( ', ', $update_sql ) . ' WHERE ' . implode( ' AND ', array_filter( $where_sql ) );
490
- $result = $this->db->query( $sql );
491
-
492
- if( !$result ) {
493
- $this->log( "Error updating row {$current_row} SQL: {$sql}", \WPStaging\Utils\Logger::TYPE_ERROR );
494
- }
495
- }
496
- } // end row loop
497
- unset( $row );
498
- unset( $update_sql );
499
- unset( $where_sql );
500
- unset( $sql );
501
- unset( $current_row );
502
-
503
-
504
- // DB Flush
505
- $this->db->flush();
506
- return true;
507
- }
508
-
509
- /**
510
- * Get path to multisite image folder e.g. wp-content/blogs.dir/ID/files or wp-content/uploads/sites/ID
511
- * @return string
512
- */
513
- private function getImagePathLive() {
514
- // Check first which structure is used
515
- $uploads = wp_upload_dir();
516
- $basedir = $uploads['basedir'];
517
- $blogId = get_current_blog_id();
518
-
519
- if( false === strpos( $basedir, 'blogs.dir' ) ) {
520
- // Since WP 3.5
521
- $path = $blogId > 1 ?
522
- 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'sites' . DIRECTORY_SEPARATOR . get_current_blog_id() . DIRECTORY_SEPARATOR :
523
- 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR;
524
- } else {
525
- // old blog structure
526
- $path = $blogId > 1 ?
527
- 'wp-content' . DIRECTORY_SEPARATOR . 'blogs.dir' . DIRECTORY_SEPARATOR . get_current_blog_id() . DIRECTORY_SEPARATOR . 'files' . DIRECTORY_SEPARATOR :
528
- 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR;
529
- }
530
- return $path;
531
- }
532
-
533
- /**
534
- * Get path to staging site image path wp-content/uploads
535
- * @return string
536
- */
537
- private function getImagePathStaging() {
538
- return 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR;
539
- }
540
-
541
- /**
542
- * Adapted from interconnect/it's search/replace script.
543
- *
544
- * @link https://interconnectit.com/products/search-and-replace-for-wordpress-databases/
545
- *
546
- * Take a serialised array and unserialise it replacing elements as needed and
547
- * unserialising any subordinate arrays and performing the replace on those too.
548
- *
549
- * @access private
550
- * @param string $from String we're looking to replace.
551
- * @param string $to What we want it to be replaced with
552
- * @param array $data Used to pass any subordinate arrays back to in.
553
- * @param boolean $serialized Does the array passed via $data need serialising.
554
- * @param sting|boolean $case_insensitive Set to 'on' if we should ignore case, false otherwise.
555
- *
556
- * @return string|array The original array with all elements replaced as needed.
557
- */
558
- private function recursive_unserialize_replace( $from = '', $to = '', $data = '', $serialized = false, $case_insensitive = false ) {
559
- try {
560
- // PDO instances can not be serialized or unserialized
561
- if( is_serialized( $data ) && strpos( $data, 'O:3:"PDO":0:' ) !== false ) {
562
- return $data;
563
- }
564
- // Some unserialized data cannot be re-serialized eg. SimpleXMLElements
565
- if( is_serialized( $data ) && ( $unserialized = @unserialize( $data ) ) !== false ) {
566
- $data = $this->recursive_unserialize_replace( $from, $to, $unserialized, true, $case_insensitive );
567
- } elseif( is_array( $data ) ) {
568
- $tmp = array();
569
- foreach ( $data as $key => $value ) {
570
- $tmp[$key] = $this->recursive_unserialize_replace( $from, $to, $value, false, $case_insensitive );
571
- }
572
-
573
- $data = $tmp;
574
- unset( $tmp );
575
- } elseif( is_object( $data ) ) {
576
- $props = get_object_vars( $data );
577
-
578
- // Do a search & replace
579
- if( empty( $props['__PHP_Incomplete_Class_Name'] ) ) {
580
- $tmp = $data;
581
- foreach ( $props as $key => $value ) {
582
- if( $key === '' || ord( $key[0] ) === 0 ) {
583
- continue;
584
- }
585
- $tmp->$key = $this->recursive_unserialize_replace( $from, $to, $value, false, $case_insensitive );
586
- }
587
- $data = $tmp;
588
- $tmp = '';
589
- $props = '';
590
- unset( $tmp );
591
- unset( $props );
592
- }
593
- } else {
594
- if( is_string( $data ) ) {
595
- if( !empty( $from ) && !empty( $to ) ) {
596
- $data = $this->str_replace( $from, $to, $data, $case_insensitive );
597
- }
598
- }
599
- }
600
-
601
- if( $serialized ) {
602
- return serialize( $data );
603
- }
604
- } catch ( Exception $error ) {
605
-
606
- }
607
-
608
- return $data;
609
- }
610
-
611
- /**
612
- * Check if the object is a valid one and not __PHP_Incomplete_Class_Name
613
- * Can not use is_object alone because in php 7.2 it's returning true even though object is __PHP_Incomplete_Class_Name
614
- * @return boolean
615
- */
616
- // private function isValidObject( $data ) {
617
- // if( !is_object( $data ) || gettype( $data ) != 'object' ) {
618
- // return false;
619
- // }
620
- //
621
- // $invalid_class_props = get_object_vars( $data );
622
- //
623
- // if (!isset($invalid_class_props['__PHP_Incomplete_Class_Name'])){
624
- // // Assume it must be an valid object
625
- // return true;
626
- // }
627
- //
628
- // $invalid_object_class = $invalid_class_props['__PHP_Incomplete_Class_Name'];
629
- //
630
- // if( !empty( $invalid_object_class ) ) {
631
- // return false;
632
- // }
633
- //
634
- // // Assume it must be an valid object
635
- // return true;
636
- // }
637
-
638
- /**
639
- * Mimics the mysql_real_escape_string function. Adapted from a post by 'feedr' on php.net.
640
- * @link http://php.net/manual/en/function.mysql-real-escape-string.php#101248
641
- * @access public
642
- * @param string $input The string to escape.
643
- * @return string
644
- */
645
- private function mysql_escape_mimic( $input ) {
646
- if( is_array( $input ) ) {
647
- return array_map( __METHOD__, $input );
648
- }
649
- if( !empty( $input ) && is_string( $input ) ) {
650
- return str_replace( array('\\', "\0", "\n", "\r", "'", '"', "\x1a"), array('\\\\', '\\0', '\\n', '\\r', "\\'", '\\"', '\\Z'), $input );
651
- }
652
-
653
- return $input;
654
- }
655
-
656
- /**
657
- * Return unserialized object or array
658
- *
659
- * @param string $serialized_string Serialized string.
660
- * @param string $method The name of the caller method.
661
- *
662
- * @return mixed, false on failure
663
- */
664
- private static function unserialize( $serialized_string ) {
665
- if( !is_serialized( $serialized_string ) ) {
666
- return false;
667
- }
668
-
669
- $serialized_string = trim( $serialized_string );
670
- $unserialized_string = @unserialize( $serialized_string );
671
-
672
- return $unserialized_string;
673
- }
674
-
675
- /**
676
- * Wrapper for str_replace
677
- *
678
- * @param string $from
679
- * @param string $to
680
- * @param string $data
681
- * @param string|bool $case_insensitive
682
- *
683
- * @return string
684
- */
685
- private function str_replace( $from, $to, $data, $case_insensitive = false ) {
686
-
687
- // Add filter
688
- $excludes = apply_filters( 'wpstg_clone_searchreplace_excl', array() );
689
-
690
- // Build pattern
691
- $regexExclude = '';
692
- foreach ( $excludes as $exclude ) {
693
- $regexExclude .= $exclude . '(*SKIP)(FAIL)|';
694
- }
695
-
696
- if( 'on' === $case_insensitive ) {
697
- //$data = str_ireplace( $from, $to, $data );
698
- $data = preg_replace( '#' . $regexExclude . preg_quote( $from ) . '#i', $to, $data );
699
- } else {
700
- //$data = str_replace( $from, $to, $data );
701
- $data = preg_replace( '#' . $regexExclude . preg_quote( $from ) . '#', $to, $data );
702
- }
703
-
704
- return $data;
705
- }
706
-
707
- /**
708
- * Set the job
709
- * @param string $table
710
- */
711
- private function setJob( $table ) {
712
- if( !empty( $this->options->job->current ) ) {
713
- return;
714
- }
715
-
716
- $this->options->job->current = $table;
717
- $this->options->job->start = 0;
718
- }
719
-
720
- /**
721
- * Start Job
722
- * @param string $new
723
- * @param string $old
724
- * @return bool
725
- */
726
- private function startJob( $new, $old ) {
727
-
728
- $this->options->job->total = 0;
729
-
730
- if( 0 != $this->options->job->start ) {
731
- return true;
732
- }
733
-
734
- // Table does not exist
735
- $result = $this->db->query( "SHOW TABLES LIKE '{$old}'" );
736
- if( !$result || 0 === $result ) {
737
- return false;
738
- }
739
-
740
- $this->options->job->total = ( int ) $this->db->get_var( "SELECT COUNT(1) FROM {$old}" );
741
-
742
- if( 0 == $this->options->job->total ) {
743
- $this->finishStep();
744
- return false;
745
- }
746
-
747
- return true;
748
- }
749
-
750
- /**
751
- * Is table excluded from search replace processing?
752
- * @param string $table
753
- * @return boolean
754
- */
755
- private function isExcludedTable( $table ) {
756
-
757
- $customTables = apply_filters( 'wpstg_clone_searchreplace_tables_exclude', array() );
758
- $defaultTables = array('blogs');
759
-
760
- $tables = array_merge( $customTables, $defaultTables );
761
-
762
- $excludedTables = array();
763
- foreach ( $tables as $key => $value ) {
764
- $excludedTables[] = $this->options->prefix . $value;
765
- }
766
-
767
- if( in_array( $table, $excludedTables ) ) {
768
- return true;
769
- }
770
- return false;
771
- }
772
-
773
- /**
774
- * Finish the step
775
- */
776
- private function finishStep() {
777
- // This job is not finished yet
778
- if( $this->options->job->total > $this->options->job->start ) {
779
- return false;
780
- }
781
-
782
- // Add it to cloned tables listing
783
- $this->options->clonedTables[] = $this->options->tables[$this->options->currentStep];
784
-
785
- // Reset job
786
- $this->options->job = new \stdClass();
787
-
788
- return true;
789
- }
790
-
791
- /**
792
- * Drop table if necessary
793
- * @param string $new
794
- */
795
- private function dropTable( $new ) {
796
- $old = $this->db->get_var( $this->db->prepare( "SHOW TABLES LIKE %s", $new ) );
797
-
798
- if( !$this->shouldDropTable( $new, $old ) ) {
799
- return;
800
- }
801
-
802
- $this->log( "DB Search & Replace: {$new} already exists, dropping it first" );
803
- $this->db->query( "DROP TABLE {$new}" );
804
- }
805
-
806
- /**
807
- * Check if table needs to be dropped
808
- * @param string $new
809
- * @param string $old
810
- * @return bool
811
- */
812
- private function shouldDropTable( $new, $old ) {
813
- return (
814
- $old == $new &&
815
- (
816
- !isset( $this->options->job->current ) ||
817
- !isset( $this->options->job->start ) ||
818
- 0 == $this->options->job->start
819
- )
820
- );
821
- }
822
-
823
- /**
824
- * Check if WP is installed in subdir
825
- * @return boolean
826
- */
827
- private function isSubDir() {
828
- // Compare names without scheme to bypass cases where siteurl and home have different schemes http / https
829
- // This is happening much more often than you would expect
830
- $siteurl = preg_replace( '#^https?://#', '', rtrim( get_option( 'siteurl' ), '/' ) );
831
- $home = preg_replace( '#^https?://#', '', rtrim( get_option( 'home' ), '/' ) );
832
-
833
- if( $home !== $siteurl ) {
834
- return true;
835
- }
836
- return false;
837
- }
838
-
839
- }
1
+ <?php
2
+
3
+ namespace WPStaging\Backend\Modules\Jobs\Multisite;
4
+
5
+ // No Direct Access
6
+ if( !defined( "WPINC" ) ) {
7
+ die;
8
+ }
9
+
10
+ use WPStaging\WPStaging;
11
+ use WPStaging\Utils\Strings;
12
+ use WPStaging\Utils\Helper;
13
+ use WPStaging\Utils\Multisite;
14
+ use WPStaging\Backend\Modules\Jobs\JobExecutable;
15
+
16
+ /**
17
+ * Class Database
18
+ * @package WPStaging\Backend\Modules\Jobs
19
+ */
20
+ class SearchReplace extends JobExecutable {
21
+
22
+ /**
23
+ * @var int
24
+ */
25
+ private $total = 0;
26
+
27
+ /**
28
+ * @var \WPDB
29
+ */
30
+ public $db;
31
+
32
+ /**
33
+ *
34
+ * @var Obj
35
+ */
36
+ private $strings;
37
+
38
+ /**
39
+ *
40
+ * @var string
41
+ */
42
+ private $destinationHostname;
43
+
44
+ /**
45
+ *
46
+ * @var string
47
+ */
48
+ private $sourceHostname;
49
+
50
+ /**
51
+ *
52
+ * @var string
53
+ */
54
+ //private $targetDir;
55
+
56
+ /**
57
+ * The prefix of the new database tables which are used for the live site after updating tables
58
+ * @var string
59
+ */
60
+ public $tmpPrefix;
61
+
62
+ /**
63
+ * Initialize
64
+ */
65
+ public function initialize() {
66
+ $this->total = count( $this->options->tables );
67
+ $this->db = WPStaging::getInstance()->get( "wpdb" );
68
+ $this->tmpPrefix = $this->options->prefix;
69
+ $this->strings = new Strings();
70
+ $this->sourceHostname = $this->getSourceHostname();
71
+ $this->destinationHostname = $this->getDestinationHostname();
72
+ }
73
+
74
+ public function start() {
75
+ // Skip job. Nothing to do
76
+ if( $this->options->totalSteps === 0 ) {
77
+ $this->prepareResponse( true, false );
78
+ }
79
+
80
+ $this->run();
81
+
82
+ // Save option, progress
83
+ $this->saveOptions();
84
+
85
+ return ( object ) $this->response;
86
+ }
87
+
88
+ /**
89
+ * Calculate Total Steps in This Job and Assign It to $this->options->totalSteps
90
+ * @return void
91
+ */
92
+ protected function calculateTotalSteps() {
93
+ $this->options->totalSteps = $this->total;
94
+ }
95
+
96
+ /**
97
+ * Execute the Current Step
98
+ * Returns false when over threshold limits are hit or when the job is done, true otherwise
99
+ * @return bool
100
+ */
101
+ protected function execute() {
102
+ // Over limits threshold
103
+ if( $this->isOverThreshold() ) {
104
+ // Prepare response and save current progress
105
+ $this->prepareResponse( false, false );
106
+ $this->saveOptions();
107
+ return false;
108
+ }
109
+
110
+ // No more steps, finished
111
+ if( $this->options->currentStep > $this->total || !isset( $this->options->tables[$this->options->currentStep] ) ) {
112
+ $this->prepareResponse( true, false );
113
+ return false;
114
+ }
115
+
116
+ // Table is excluded
117
+ if( in_array( $this->options->tables[$this->options->currentStep], $this->options->excludedTables ) ) {
118
+ $this->prepareResponse();
119
+ return true;
120
+ }
121
+
122
+ // Search & Replace
123
+ if( !$this->stopExecution() && !$this->updateTable( $this->options->tables[$this->options->currentStep] ) ) {
124
+ // Prepare Response
125
+ $this->prepareResponse( false, false );
126
+
127
+ // Not finished
128
+ return true;
129
+ }
130
+
131
+
132
+ // Prepare Response
133
+ $this->prepareResponse();
134
+
135
+ // Not finished
136
+ return true;
137
+ }
138
+
139
+ /**
140
+ * Stop Execution immediately
141
+ * return mixed bool | json
142
+ */
143
+ private function stopExecution() {
144
+ if( $this->db->prefix == $this->tmpPrefix ) {
145
+ $this->returnException( 'Fatal Error 9: Prefix ' . $this->db->prefix . ' is used for the live site hence it can not be used for the staging site as well. Please ask support@wp-staging.com how to resolve this.' );
146
+ }
147
+ return false;
148
+ }
149
+
150
+ /**
151
+ * Copy Tables
152
+ * @param string $tableName
153
+ * @return bool
154
+ */
155
+ private function updateTable( $tableName ) {
156
+ $strings = new Strings();
157
+ $table = $strings->str_replace_first( $this->db->prefix, '', $tableName );
158
+ $newTableName = $this->tmpPrefix . $table;
159
+
160
+ // Save current job
161
+ $this->setJob( $newTableName );
162
+
163
+ // Beginning of the job
164
+ if( !$this->startJob( $newTableName, $tableName ) ) {
165
+ return true;
166
+ }
167
+ // Copy data
168
+ $this->startReplace( $newTableName );
169
+
170
+ // Finish the step
171
+ return $this->finishStep();
172
+ }
173
+
174
+ /**
175
+ * Get source Hostname depending on wheather WP has been installed in sub dir or not
176
+ * @return type
177
+ */
178
+ public function getSourceHostname() {
179
+
180
+ if( $this->isSubDir() ) {
181
+ return trailingslashit( $this->multisiteHomeUrlWithoutScheme ) . '/' . $this->getSubDir();
182
+ }
183
+ return $this->multisiteHomeUrlWithoutScheme;
184
+ }
185
+
186
+ /**
187
+ * Get destination Hostname depending on wheather WP has been installed in sub dir or not
188
+ * @return type
189
+ */
190
+ public function getDestinationHostname() {
191
+
192
+ if( !empty( $this->options->cloneHostname ) ) {
193
+ return $this->strings->getUrlWithoutScheme( $this->options->cloneHostname );
194
+ }
195
+
196
+ if( $this->isSubDir() ) {
197
+ return trailingslashit( $this->strings->getUrlWithoutScheme( $this->multisiteDomainWithoutScheme ) ) . $this->getSubDir() . '/' . $this->options->cloneDirectoryName;
198
+ }
199
+
200
+ // Get the path to the main multisite without appending and trailingslash e.g. wordpress
201
+ $multisitePath = defined( 'PATH_CURRENT_SITE' ) ? PATH_CURRENT_SITE : '/';
202
+ $url = rtrim( $this->strings->getUrlWithoutScheme( $this->multisiteDomainWithoutScheme ), '/\\' ) . $multisitePath . $this->options->cloneDirectoryName;
203
+ //$multisitePath = defined( 'PATH_CURRENT_SITE' ) ? str_replace( '/', '', PATH_CURRENT_SITE ) : '';
204
+ //$url = trailingslashit( $this->strings->getUrlWithoutScheme( $this->multisiteDomainWithoutScheme ) ) . $multisitePath . '/' . $this->options->cloneDirectoryName;
205
+ return $url;
206
+ }
207
+
208
+ /**
209
+ * Get the install sub directory if WP is installed in sub directory
210
+ * @return string
211
+ */
212
+ private function getSubDir() {
213
+ $home = get_option( 'home' );
214
+ $siteurl = get_option( 'siteurl' );
215
+
216
+ if( empty( $home ) || empty( $siteurl ) ) {
217
+ return '';
218
+ }
219
+
220
+ $dir = str_replace( $home, '', $siteurl );
221
+ return str_replace( '/', '', $dir );
222
+ }
223
+
224
+ /**
225
+ * Start search replace job
226
+ * @param string $new
227
+ * @param string $old
228
+ */
229
+ private function startReplace( $table ) {
230
+ $rows = $this->options->job->start + $this->settings->querySRLimit;
231
+ $this->log(
232
+ "DB Search & Replace: Table {$table} {$this->options->job->start} to {$rows} records"
233
+ );
234
+
235
+ // Search & Replace
236
+ $this->searchReplace( $table, $rows, array() );
237
+
238
+ // Set new offset
239
+ $this->options->job->start += $this->settings->querySRLimit;
240
+ }
241
+
242
+ /**
243
+ * Returns the number of pages in a table.
244
+ * @access public
245
+ * @return int
246
+ */
247
+ // private function get_pages_in_table( $table ) {
248
+ //
249
+ // // Table does not exist
250
+ // $result = $this->db->query( "SHOW TABLES LIKE '{$table}'" );
251
+ // if( !$result || 0 === $result ) {
252
+ // return 0;
253
+ // }
254
+ //
255
+ // $table = esc_sql( $table );
256
+ // $rows = $this->db->get_var( "SELECT COUNT(*) FROM $table" );
257
+ // $pages = ceil( $rows / $this->settings->querySRLimit );
258
+ // return absint( $pages );
259
+ // }
260
+
261
+ /**
262
+ * Gets the columns in a table.
263
+ * @access public
264
+ * @param string $table The table to check.
265
+ * @return array
266
+ */
267
+ private function get_columns( $table ) {
268
+ $primary_key = null;
269
+ $columns = array();
270
+ $fields = $this->db->get_results( 'DESCRIBE ' . $table );
271
+ if( is_array( $fields ) ) {
272
+ foreach ( $fields as $column ) {
273
+ $columns[] = $column->Field;
274
+ if( $column->Key == 'PRI' ) {
275
+ $primary_key = $column->Field;
276
+ }
277
+ }
278
+ }
279
+ return array($primary_key, $columns);
280
+ }
281
+
282
+ /**
283
+ * Return absolute destination path
284
+ * @return string
285
+ */
286
+ private function getAbsDestination() {
287
+ if( empty( $this->options->cloneDir ) ) {
288
+ return \WPStaging\WPStaging::getWPpath();
289
+ }
290
+ return trailingslashit( $this->options->cloneDir );
291
+ }
292
+
293
+ /**
294
+ * Adapated from interconnect/it's search/replace script, adapted from Better Search Replace
295
+ *
296
+ * Modified to use WordPress wpdb functions instead of PHP's native mysql/pdo functions,
297
+ * and to be compatible with batch processing.
298
+ *
299
+ * @link https://interconnectit.com/products/search-and-replace-for-wordpress-databases/
300
+ *
301
+ * @access public
302
+ * @param string $table The table to run the replacement on.
303
+ * @param int $page The page/block to begin the query on.
304
+ * @param array $args An associative array containing arguments for this run.
305
+ * @return array
306
+ */
307
+ private function searchReplace( $table, $page, $args ) {
308
+
309
+ if( $this->thirdParty->isSearchReplaceExcluded( $table ) ) {
310
+ $this->log( "DB Search & Replace: Skip {$table}", \WPStaging\Utils\Logger::TYPE_INFO );
311
+ return true;
312
+ }
313
+
314
+ // Load up the default settings for this chunk.
315
+ $table = esc_sql( $table );
316
+ //$current_page = $this->options->job->start + $this->settings->querySRLimit;
317
+ //$pages = $this->get_pages_in_table( $table );
318
+ // Search URL example.com/staging and root path to staging site /var/www/htdocs/staging
319
+ $args['search_for'] = array(
320
+ '//' . $this->getSourceHostname(),
321
+ ABSPATH,
322
+ '\/\/' . str_replace( '/', '\/', $this->getSourceHostname() ), // Used by revslider and several visual editors
323
+ '%2F%2F' . str_replace( '/', '%2F', $this->getSourceHostname() ), // HTML entitity for WP Backery Page Builder Plugin
324
+ //$this->getImagePathLive()
325
+ );
326
+
327
+
328
+ $args['replace_with'] = array(
329
+ '//' . $this->getDestinationHostname(),
330
+ $this->options->destinationDir,
331
+ '\/\/' . str_replace( '/', '\/', $this->getDestinationHostname() ), // Used by revslider and several visual editors
332
+ '%2F%2F' . str_replace( '/', '%2F', $this->getDestinationHostname() ), // HTML entitity for WP Backery Page Builder Plugin
333
+ //$this->getImagePathStaging()
334
+ );
335
+
336
+ $this->debugLog( "DB Search & Replace: Search: {$args['search_for'][0]}", \WPStaging\Utils\Logger::TYPE_INFO );
337
+ $this->debugLog( "DB Search & Replace: Replace: {$args['replace_with'][0]}", \WPStaging\Utils\Logger::TYPE_INFO );
338
+
339
+
340
+
341
+ $args['replace_guids'] = 'off';
342
+ $args['dry_run'] = 'off';
343
+ $args['case_insensitive'] = false;
344
+ //$args['replace_mails'] = 'off';
345
+ $args['skip_transients'] = 'on';
346
+
347
+
348
+ // Allow filtering of search & replace parameters
349
+ $args = apply_filters( 'wpstg_clone_searchreplace_params', $args );
350
+
351
+ // Get a list of columns in this table.
352
+ list( $primary_key, $columns ) = $this->get_columns( $table );
353
+
354
+ // Bail out early if there isn't a primary key.
355
+ // We commented this to search & replace through tables which have no primary keys like wp_revslider_slides
356
+ // @todo test this carefully. If it causes (performance) issues we need to activate it again!
357
+ // @since 2.4.4
358
+ // if( null === $primary_key ) {
359
+ // return false;
360
+ // }
361
+
362
+ $current_row = 0;
363
+ $start = $this->options->job->start;
364
+ $end = $this->settings->querySRLimit;
365
+
366
+ // Grab the content of the table.
367
+ $data = $this->db->get_results( "SELECT * FROM $table LIMIT $start, $end", ARRAY_A );
368
+
369
+ // Filter certain rows option_name in wpstg_options
370
+ $filter = array(
371
+ 'Admin_custome_login_Slidshow',
372
+ 'Admin_custome_login_Social',
373
+ 'Admin_custome_login_logo',
374
+ 'Admin_custome_login_text',
375
+ 'Admin_custome_login_login',
376
+ 'Admin_custome_login_top',
377
+ 'Admin_custome_login_dashboard',
378
+ 'Admin_custome_login_Version',
379
+ 'upload_path'
380
+ );
381
+
382
+ $filter = apply_filters( 'wpstg_clone_searchreplace_excl_rows', $filter );
383
+
384
+ // Loop through the data.
385
+ foreach ( $data as $row ) {
386
+ $current_row++;
387
+ $update_sql = array();
388
+ $where_sql = array();
389
+ $upd = false;
390
+
391
+ // Skip rows below
392
+ if( isset( $row['option_name'] ) && in_array( $row['option_name'], $filter ) ) {
393
+ continue;
394
+ }
395
+
396
+ // Skip rows with transients (They can store huge data and we need to save memory)
397
+ if( isset( $row['option_name'] ) && 'on' === $args['skip_transients'] && false !== strpos( $row['option_name'], '_transient' ) ) {
398
+ continue;
399
+ }
400
+ // Skip rows with more than 5MB to save memory
401
+ if( isset( $row['option_value'] ) && strlen( $row['option_value'] ) >= 5000000 ) {
402
+ continue;
403
+ }
404
+
405
+
406
+ foreach ( $columns as $column ) {
407
+
408
+ $dataRow = $row[$column];
409
+
410
+ // Skip rows larger than 5MB
411
+ $size = strlen( $dataRow );
412
+ if( $size >= 5000000 ) {
413
+ continue;
414
+ }
415
+
416
+ // Skip Primary key
417
+ if( $column == $primary_key ) {
418
+ $where_sql[] = $column . ' = "' . $this->mysql_escape_mimic( $dataRow ) . '"';
419
+ continue;
420
+ }
421
+
422
+ // Skip GUIDs by default.
423
+ if( 'on' !== $args['replace_guids'] && 'guid' == $column ) {
424
+ continue;
425
+ }
426
+
427
+ // Skip mail addresses
428
+ // if( 'off' === $args['replace_mails'] && false !== strpos( $dataRow, '@' . $this->multisiteDomainWithoutScheme ) ) {
429
+ // continue;
430
+ // }
431
+ // Check options table
432
+ if( $this->options->prefix . 'options' === $table ) {
433
+
434
+ // Skip certain options
435
+ // if( isset( $should_skip ) && true === $should_skip ) {
436
+ // $should_skip = false;
437
+ // continue;
438
+ // }
439
+ // Skip this row
440
+ if( 'wpstg_existing_clones_beta' === $dataRow ||
441
+ 'wpstg_existing_clones' === $dataRow ||
442
+ 'wpstg_settings' === $dataRow ||
443
+ 'wpstg_license_status' === $dataRow ||
444
+ 'siteurl' === $dataRow ||
445
+ 'home' === $dataRow
446
+ ) {
447
+ //$should_skip = true;
448
+ continue;
449
+ }
450
+ }
451
+
452
+ // Check the path delimiter for / or \/ and remove one of those which prevents from resulting in wrong syntax like domain.com/staging\/.
453
+ // 1. local.wordpress.test -> local.wordpress.test/staging
454
+ // 2. local.wordpress.test\/ -> local.wordpress.test\/staging\/
455
+ $tmp = $args;
456
+ if( false === strpos( $dataRow, $tmp['search_for'][0] ) ) {
457
+ array_shift( $tmp['search_for'] ); // rtrim( $this->homeUrl, '/' ),
458
+ array_shift( $tmp['replace_with'] ); // rtrim( $this->homeUrl, '/' ) . '/' . $this->options->cloneDirectoryName,
459
+ } else {
460
+ unset( $tmp['search_for'][1] );
461
+ unset( $tmp['replace_with'][1] );
462
+ // recount array
463
+ $tmp['search_for'] = array_values( $tmp['search_for'] );
464
+ $tmp['replace_with'] = array_values( $tmp['replace_with'] );
465
+ }
466
+
467
+ // Run a search replace on the data row and respect the serialisation.
468
+ $i = 0;
469
+ foreach ( $tmp['search_for'] as $replace ) {
470
+ $dataRow = $this->recursive_unserialize_replace( $tmp['search_for'][$i], $tmp['replace_with'][$i], $dataRow, false, $args['case_insensitive'] );
471
+ $i++;
472
+ }
473
+ unset( $replace );
474
+ unset( $i );
475
+ unset( $tmp );
476
+
477
+ // Something was changed
478
+ if( $row[$column] != $dataRow ) {
479
+ $update_sql[] = $column . ' = "' . $this->mysql_escape_mimic( $dataRow ) . '"';
480
+ $upd = true;
481
+ }
482
+ }
483
+
484
+ // Determine what to do with updates.
485
+ if( $args['dry_run'] === 'on' ) {
486
+ // Don't do anything if a dry run
487
+ } elseif( $upd && !empty( $where_sql ) ) {
488
+ // If there are changes to make, run the query.
489
+ $sql = 'UPDATE ' . $table . ' SET ' . implode( ', ', $update_sql ) . ' WHERE ' . implode( ' AND ', array_filter( $where_sql ) );
490
+ $result = $this->db->query( $sql );
491
+
492
+ if( !$result ) {
493
+ $this->log( "Error updating row {$current_row} SQL: {$sql}", \WPStaging\Utils\Logger::TYPE_ERROR );
494
+ }
495
+ }
496
+ } // end row loop
497
+ unset( $row );
498
+ unset( $update_sql );
499
+ unset( $where_sql );
500
+ unset( $sql );
501
+ unset( $current_row );
502
+
503
+
504
+ // DB Flush
505
+ $this->db->flush();
506
+ return true;
507
+ }
508
+
509
+ /**
510
+ * Get path to multisite image folder e.g. wp-content/blogs.dir/ID/files or wp-content/uploads/sites/ID
511
+ * @return string
512
+ */
513
+ private function getImagePathLive() {
514
+ // Check first which structure is used
515
+ $uploads = wp_upload_dir();
516
+ $basedir = $uploads['basedir'];
517
+ $blogId = get_current_blog_id();
518
+
519
+ if( false === strpos( $basedir, 'blogs.dir' ) ) {
520
+ // Since WP 3.5
521
+ $path = $blogId > 1 ?
522
+ 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'sites' . DIRECTORY_SEPARATOR . get_current_blog_id() . DIRECTORY_SEPARATOR :
523
+ 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR;
524
+ } else {
525
+ // old blog structure
526
+ $path = $blogId > 1 ?
527
+ 'wp-content' . DIRECTORY_SEPARATOR . 'blogs.dir' . DIRECTORY_SEPARATOR . get_current_blog_id() . DIRECTORY_SEPARATOR . 'files' . DIRECTORY_SEPARATOR :
528
+ 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR;
529
+ }
530
+ return $path;
531
+ }
532
+
533
+ /**
534
+ * Get path to staging site image path wp-content/uploads
535
+ * @return string
536
+ */
537
+ private function getImagePathStaging() {
538
+ return 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR;
539
+ }
540
+
541
+ /**
542
+ * Adapted from interconnect/it's search/replace script.
543
+ *
544
+ * @link https://interconnectit.com/products/search-and-replace-for-wordpress-databases/
545
+ *
546
+ * Take a serialised array and unserialise it replacing elements as needed and
547
+ * unserialising any subordinate arrays and performing the replace on those too.
548
+ *
549
+ * @access private
550
+ * @param string $from String we're looking to replace.
551
+ * @param string $to What we want it to be replaced with
552
+ * @param array $data Used to pass any subordinate arrays back to in.
553
+ * @param boolean $serialized Does the array passed via $data need serialising.
554
+ * @param sting|boolean $case_insensitive Set to 'on' if we should ignore case, false otherwise.
555
+ *
556
+ * @return string|array The original array with all elements replaced as needed.
557
+ */
558
+ private function recursive_unserialize_replace( $from = '', $to = '', $data = '', $serialized = false, $case_insensitive = false ) {
559
+ try {
560
+ // PDO instances can not be serialized or unserialized
561
+ if( is_serialized( $data ) && strpos( $data, 'O:3:"PDO":0:' ) !== false ) {
562
+ return $data;
563
+ }
564
+ // Some unserialized data cannot be re-serialized eg. SimpleXMLElements
565
+ if( is_serialized( $data ) && ( $unserialized = @unserialize( $data ) ) !== false ) {
566
+ $data = $this->recursive_unserialize_replace( $from, $to, $unserialized, true, $case_insensitive );
567
+ } elseif( is_array( $data ) ) {
568
+ $tmp = array();
569
+ foreach ( $data as $key => $value ) {
570
+ $tmp[$key] = $this->recursive_unserialize_replace( $from, $to, $value, false, $case_insensitive );
571
+ }
572
+
573
+ $data = $tmp;
574
+ unset( $tmp );
575
+ } elseif( is_object( $data ) ) {
576
+ $props = get_object_vars( $data );
577
+
578
+ // Do a search & replace
579
+ if( empty( $props['__PHP_Incomplete_Class_Name'] ) ) {
580
+ $tmp = $data;
581
+ foreach ( $props as $key => $value ) {
582
+ if( $key === '' || ord( $key[0] ) === 0 ) {
583
+ continue;
584
+ }
585
+ $tmp->$key = $this->recursive_unserialize_replace( $from, $to, $value, false, $case_insensitive );
586
+ }
587
+ $data = $tmp;
588
+ $tmp = '';
589
+ $props = '';
590
+ unset( $tmp );
591
+ unset( $props );
592
+ }
593
+ } else {
594
+ if( is_string( $data ) ) {
595
+ if( !empty( $from ) && !empty( $to ) ) {
596
+ $data = $this->str_replace( $from, $to, $data, $case_insensitive );
597
+ }
598
+ }
599
+ }
600
+
601
+ if( $serialized ) {
602
+ return serialize( $data );
603
+ }
604
+ } catch ( Exception $error ) {
605
+
606
+ }
607
+
608
+ return $data;
609
+ }
610
+
611
+ /**
612
+ * Check if the object is a valid one and not __PHP_Incomplete_Class_Name
613
+ * Can not use is_object alone because in php 7.2 it's returning true even though object is __PHP_Incomplete_Class_Name
614
+ * @return boolean
615
+ */
616
+ // private function isValidObject( $data ) {
617
+ // if( !is_object( $data ) || gettype( $data ) != 'object' ) {
618
+ // return false;
619
+ // }
620
+ //
621
+ // $invalid_class_props = get_object_vars( $data );
622
+ //
623
+ // if (!isset($invalid_class_props['__PHP_Incomplete_Class_Name'])){
624
+ // // Assume it must be an valid object
625
+ // return true;
626
+ // }
627
+ //
628
+ // $invalid_object_class = $invalid_class_props['__PHP_Incomplete_Class_Name'];
629
+ //
630
+ // if( !empty( $invalid_object_class ) ) {
631
+ // return false;
632
+ // }
633
+ //
634
+ // // Assume it must be an valid object
635
+ // return true;
636
+ // }
637
+
638
+ /**
639
+ * Mimics the mysql_real_escape_string function. Adapted from a post by 'feedr' on php.net.
640
+ * @link http://php.net/manual/en/function.mysql-real-escape-string.php#101248
641
+ * @access public
642
+ * @param string $input The string to escape.
643
+ * @return string
644
+ */
645
+ private function mysql_escape_mimic( $input ) {
646
+ if( is_array( $input ) ) {
647
+ return array_map( __METHOD__, $input );
648
+ }
649
+ if( !empty( $input ) && is_string( $input ) ) {
650
+ return str_replace( array('\\', "\0", "\n", "\r", "'", '"', "\x1a"), array('\\\\', '\\0', '\\n', '\\r', "\\'", '\\"', '\\Z'), $input );
651
+ }
652
+
653
+ return $input;
654
+ }
655
+
656
+ /**
657
+ * Return unserialized object or array
658
+ *
659
+ * @param string $serialized_string Serialized string.
660
+ * @param string $method The name of the caller method.
661
+ *
662
+ * @return mixed, false on failure
663
+ */
664
+ private static function unserialize( $serialized_string ) {
665
+ if( !is_serialized( $serialized_string ) ) {
666
+ return false;
667
+ }
668
+
669
+ $serialized_string = trim( $serialized_string );
670
+ $unserialized_string = @unserialize( $serialized_string );
671
+
672
+ return $unserialized_string;
673
+ }
674
+
675
+ /**
676
+ * Wrapper for str_replace
677
+ *
678
+ * @param string $from
679
+ * @param string $to
680
+ * @param string $data
681
+ * @param string|bool $case_insensitive
682
+ *
683
+ * @return string
684
+ */
685
+ private function str_replace( $from, $to, $data, $case_insensitive = false ) {
686
+
687
+ // Add filter
688
+ $excludes = apply_filters( 'wpstg_clone_searchreplace_excl', array() );
689
+
690
+ // Build pattern
691
+ $regexExclude = '';
692
+ foreach ( $excludes as $exclude ) {
693
+ $regexExclude .= $exclude . '(*SKIP)(FAIL)|';
694
+ }
695
+
696
+ if( 'on' === $case_insensitive ) {
697
+ //$data = str_ireplace( $from, $to, $data );
698
+ $data = preg_replace( '#' . $regexExclude . preg_quote( $from ) . '#i', $to, $data );
699
+ } else {
700
+ //$data = str_replace( $from, $to, $data );
701
+ $data = preg_replace( '#' . $regexExclude . preg_quote( $from ) . '#', $to, $data );
702
+ }
703
+
704
+ return $data;
705
+ }
706
+
707
+ /**
708
+ * Set the job
709
+ * @param string $table
710
+ */
711
+ private function setJob( $table ) {
712
+ if( !empty( $this->options->job->current ) ) {
713
+ return;
714
+ }
715
+
716
+ $this->options->job->current = $table;
717
+ $this->options->job->start = 0;
718
+ }
719
+
720
+ /**
721
+ * Start Job
722
+ * @param string $new
723
+ * @param string $old
724
+ * @return bool
725
+ */
726
+ private function startJob( $new, $old ) {
727
+
728
+ $this->options->job->total = 0;
729
+
730
+ if( 0 != $this->options->job->start ) {
731
+ return true;
732
+ }
733
+
734
+ // Table does not exist
735
+ $result = $this->db->query( "SHOW TABLES LIKE '{$old}'" );
736
+ if( !$result || 0 === $result ) {
737
+ return false;
738
+ }
739
+
740
+ $this->options->job->total = ( int ) $this->db->get_var( "SELECT COUNT(1) FROM {$old}" );
741
+
742
+ if( 0 == $this->options->job->total ) {
743
+ $this->finishStep();
744
+ return false;
745
+ }
746
+
747
+ return true;
748
+ }
749
+
750
+ /**
751
+ * Is table excluded from search replace processing?
752
+ * @param string $table
753
+ * @return boolean
754
+ */
755
+ private function isExcludedTable( $table ) {
756
+
757
+ $customTables = apply_filters( 'wpstg_clone_searchreplace_tables_exclude', array() );
758
+ $defaultTables = array('blogs');
759
+
760
+ $tables = array_merge( $customTables, $defaultTables );
761
+
762
+ $excludedTables = array();
763
+ foreach ( $tables as $key => $value ) {
764
+ $excludedTables[] = $this->options->prefix . $value;
765
+ }
766
+
767
+ if( in_array( $table, $excludedTables ) ) {
768
+ return true;
769
+ }
770
+ return false;
771
+ }
772
+
773
+ /**
774
+ * Finish the step
775
+ */
776
+ private function finishStep() {
777
+ // This job is not finished yet
778
+ if( $this->options->job->total > $this->options->job->start ) {
779
+ return false;
780
+ }
781
+
782
+ // Add it to cloned tables listing
783
+ $this->options->clonedTables[] = $this->options->tables[$this->options->currentStep];
784
+
785
+ // Reset job
786
+ $this->options->job = new \stdClass();
787
+
788
+ return true;
789
+ }
790
+
791
+ /**
792
+ * Drop table if necessary
793
+ * @param string $new
794
+ */
795
+ private function dropTable( $new ) {
796
+ $old = $this->db->get_var( $this->db->prepare( "SHOW TABLES LIKE %s", $new ) );
797
+
798
+ if( !$this->shouldDropTable( $new, $old ) ) {
799
+ return;
800
+ }
801
+
802
+ $this->log( "DB Search & Replace: {$new} already exists, dropping it first" );
803
+ $this->db->query( "DROP TABLE {$new}" );
804
+ }
805
+
806
+ /**
807
+ * Check if table needs to be dropped
808
+ * @param string $new
809
+ * @param string $old
810
+ * @return bool
811
+ */
812
+ private function shouldDropTable( $new, $old ) {
813
+ return (
814
+ $old == $new &&
815
+ (
816
+ !isset( $this->options->job->current ) ||
817
+ !isset( $this->options->job->start ) ||
818
+ 0 == $this->options->job->start
819
+ )
820
+ );
821
+ }
822
+
823
+ /**
824
+ * Check if WP is installed in subdir
825
+ * @return boolean
826
+ */
827
+ private function isSubDir() {
828
+ // Compare names without scheme to bypass cases where siteurl and home have different schemes http / https
829
+ // This is happening much more often than you would expect
830
+ $siteurl = preg_replace( '#^https?://#', '', rtrim( get_option( 'siteurl' ), '/' ) );
831
+ $home = preg_replace( '#^https?://#', '', rtrim( get_option( 'home' ), '/' ) );
832
+
833
+ if( $home !== $siteurl ) {
834
+ return true;
835
+ }
836
+ return false;
837
+ }
838
+
839
+ }
apps/Backend/Modules/Jobs/Multisite/SearchReplaceExternal.php CHANGED
@@ -1,833 +1,833 @@
1
- <?php
2
-
3
- namespace WPStaging\Backend\Modules\Jobs\Multisite;
4
-
5
- // No Direct Access
6
- if( !defined( "WPINC" ) ) {
7
- die;
8
- }
9
-
10
- use WPStaging\WPStaging;
11
- use WPStaging\Utils\Strings;
12
- use WPStaging\Backend\Modules\Jobs\JobExecutable;
13
-
14
- /**
15
- * Class Database
16
- * @package WPStaging\Backend\Modules\Jobs
17
- */
18
- class SearchReplaceExternal extends JobExecutable {
19
-
20
- /**
21
- * @var int
22
- */
23
- private $total = 0;
24
-
25
- /**
26
- * Staging Site DB
27
- * @var \WPDB
28
- */
29
- private $stagingDb;
30
-
31
- /**
32
- * Production Site DB
33
- * @var \WPDB
34
- */
35
- private $productionDb;
36
-
37
- /**
38
- *
39
- * @var string
40
- */
41
- private $sourceHostname;
42
-
43
- /**
44
- *
45
- * @var string
46
- */
47
- private $destinationHostname;
48
-
49
- /**
50
- *
51
- * @var Obj
52
- */
53
- private $strings;
54
-
55
- /**
56
- * The prefix of the new database tables which are used for the live site after updating tables
57
- * @var string
58
- */
59
- public $tmpPrefix;
60
-
61
- /**
62
- * Initialize
63
- */
64
- public function initialize() {
65
- $this->total = count( $this->options->tables );
66
- $this->stagingDb = $this->getStagingDB();
67
- $this->productionDb = WPStaging::getInstance()->get( "wpdb" );
68
- $this->tmpPrefix = $this->options->prefix;
69
- $this->strings = new Strings();
70
- $this->sourceHostname = $this->getSourceHostname();
71
- $this->destinationHostname = $this->getDestinationHostname();
72
- }
73
-
74
- /**
75
- * Get database object to interact with
76
- */
77
- private function getStagingDB() {
78
- return new \wpdb( $this->options->databaseUser, $this->options->databasePassword, $this->options->databaseDatabase, $this->options->databaseServer );
79
- }
80
-
81
- public function start() {
82
- // Skip job. Nothing to do
83
- if( $this->options->totalSteps === 0 ) {
84
- $this->prepareResponse( true, false );
85
- }
86
-
87
- $this->run();
88
-
89
- // Save option, progress
90
- $this->saveOptions();
91
-
92
- return ( object ) $this->response;
93
- }
94
-
95
- /**
96
- * Calculate Total Steps in This Job and Assign It to $this->options->totalSteps
97
- * @return void
98
- */
99
- protected function calculateTotalSteps() {
100
- $this->options->totalSteps = $this->total;
101
- }
102
-
103
- /**
104
- * Execute the Current Step
105
- * Returns false when over threshold limits are hit or when the job is done, true otherwise
106
- * @return bool
107
- */
108
- protected function execute() {
109
- // Over limits threshold
110
- if( $this->isOverThreshold() ) {
111
- // Prepare response and save current progress
112
- $this->prepareResponse( false, false );
113
- $this->saveOptions();
114
- return false;
115
- }
116
-
117
- // No more steps, finished
118
- if( $this->options->currentStep > $this->total || !isset( $this->options->tables[$this->options->currentStep] ) ) {
119
- $this->prepareResponse( true, false );
120
- return false;
121
- }
122
-
123
- // Table is excluded
124
- if( in_array( $this->options->tables[$this->options->currentStep], $this->options->excludedTables ) ) {
125
- $this->prepareResponse();
126
- return true;
127
- }
128
-
129
- // Search & Replace
130
- if( !$this->stopExecution() && !$this->updateTable( $this->options->tables[$this->options->currentStep] ) ) {
131
- // Prepare Response
132
- $this->prepareResponse( false, false );
133
-
134
- // Not finished
135
- return true;
136
- }
137
-
138
-
139
- // Prepare Response
140
- $this->prepareResponse();
141
-
142
- // Not finished
143
- return true;
144
- }
145
-
146
- /**
147
- * Stop Execution immediately
148
- * return mixed bool | json
149
- */
150
- private function stopExecution() {
151
- // if( $this->stagingDb->prefix == $this->tmpPrefix ) {
152
- // $this->returnException( 'Fatal Error 9: Prefix ' . $this->stagingDb->prefix . ' is used for the live site hence it can not be used for the staging site as well. Please ask support@wp-staging.com how to resolve this.' );
153
- // }
154
- return false;
155
- }
156
-
157
- /**
158
- * Copy Tables
159
- * @param string $tableName
160
- * @return bool
161
- */
162
- private function updateTable( $tableName ) {
163
- $strings = new Strings();
164
- $table = $strings->str_replace_first( $this->productionDb->prefix, '', $tableName );
165
- $newTableName = $this->tmpPrefix . $table;
166
-
167
- // Save current job
168
- $this->setJob( $newTableName );
169
-
170
- // Beginning of the job
171
- if( !$this->startJob( $newTableName, $tableName ) ) {
172
- return true;
173
- }
174
- // Copy data
175
- $this->startReplace( $newTableName );
176
-
177
- // Finish the step
178
- return $this->finishStep();
179
- }
180
-
181
- /**
182
- * Get source Hostname depending on wheather WP has been installed in sub dir or not
183
- * @return type
184
- */
185
- private function getSourceHostname() {
186
-
187
- if( $this->isSubDir() ) {
188
- return trailingslashit( $this->multisiteHomeUrlWithoutScheme ) . '/' . $this->getSubDir();
189
- }
190
- return $this->multisiteHomeUrlWithoutScheme;
191
- }
192
-
193
- /**
194
- * Get destination Hostname depending on wheather WP has been installed in sub dir or not
195
- * Retun host name without scheme
196
- * @return type
197
- */
198
- private function getDestinationHostname() {
199
-
200
- if( !empty( $this->options->cloneHostname ) ) {
201
- return $this->strings->getUrlWithoutScheme( $this->options->cloneHostname );
202
- }
203
-
204
- if( $this->isSubDir() ) {
205
- return trailingslashit( $this->strings->getUrlWithoutScheme( $this->multisiteDomainWithoutScheme ) ) . $this->getSubDir() . '/' . $this->options->cloneDirectoryName;
206
- }
207
- return trailingslashit( $this->strings->getUrlWithoutScheme( $this->multisiteDomainWithoutScheme ) ) . $this->options->cloneDirectoryName;
208
- }
209
-
210
- /**
211
- * Get the install sub directory if WP is installed in sub directory
212
- * @return string
213
- */
214
- private function getSubDir() {
215
- $home = get_option( 'home' );
216
- $siteurl = get_option( 'siteurl' );
217
-
218
- if( empty( $home ) || empty( $siteurl ) ) {
219
- return '';
220
- }
221
-
222
- $dir = str_replace( $home, '', $siteurl );
223
- return str_replace( '/', '', $dir );
224
- }
225
-
226
- /**
227
- * Start search replace job
228
- * @param string $new
229
- * @param string $old
230
- */
231
- private function startReplace( $table ) {
232
- $rows = $this->options->job->start + $this->settings->querySRLimit;
233
- $this->log(
234
- "DB Search & Replace: Table {$table} {$this->options->job->start} to {$rows} records"
235
- );
236
-
237
- // Search & Replace
238
- $this->searchReplace( $table, $rows, array() );
239
-
240
- // Set new offset
241
- $this->options->job->start += $this->settings->querySRLimit;
242
- }
243
-
244
- /**
245
- * Returns the number of pages in a table.
246
- * @access public
247
- * @return int
248
- */
249
- // private function get_pages_in_table( $table ) {
250
- //
251
- // // Table does not exist
252
- // $table = str_replace( $this->options->prefix . '.', null, $table );
253
- // $result = $this->productionDb->query( "SHOW TABLES LIKE '{$table}'" );
254
- // if( !$result || 0 === $result ) {
255
- // return 0;
256
- // }
257
- //
258
- // $table = esc_sql( $table );
259
- // $rows = $this->productionDb->get_var( "SELECT COUNT(*) FROM $table" );
260
- // $pages = ceil( $rows / $this->settings->querySRLimit );
261
- // return absint( $pages );
262
- // }
263
-
264
- /**
265
- * Gets the columns in a table.
266
- * @access public
267
- * @param string $table The table to check.
268
- * @return array
269
- */
270
- private function get_columns( $table ) {
271
- $primary_key = null;
272
- $columns = array();
273
- $fields = $this->stagingDb->get_results( 'DESCRIBE ' . $table );
274
- if( is_array( $fields ) ) {
275
- foreach ( $fields as $column ) {
276
- $columns[] = $column->Field;
277
- if( $column->Key == 'PRI' ) {
278
- $primary_key = $column->Field;
279
- }
280
- }
281
- }
282
- return array($primary_key, $columns);
283
- }
284
-
285
- /**
286
- * Adapated from interconnect/it's search/replace script, adapted from Better Search Replace
287
- *
288
- * Modified to use WordPress wpdb functions instead of PHP's native mysql/pdo functions,
289
- * and to be compatible with batch processing.
290
- *
291
- * @link https://interconnectit.com/products/search-and-replace-for-wordpress-databases/
292
- *
293
- * @access public
294
- * @param string $table The table to run the replacement on.
295
- * @param int $page The page/block to begin the query on.
296
- * @param array $args An associative array containing arguments for this run.
297
- * @return array
298
- */
299
- private function searchReplace( $table, $page, $args ) {
300
-
301
- if( $this->thirdParty->isSearchReplaceExcluded( $table ) ) {
302
- $this->log( "DB Search & Replace: Skip {$table}", \WPStaging\Utils\Logger::TYPE_INFO );
303
- return true;
304
- }
305
-
306
- // Load up the default settings for this chunk.
307
- $table = esc_sql( $table );
308
- //$current_page = $this->options->job->start + $this->settings->querySRLimit;
309
- //$pages = $this->get_pages_in_table( $table );
310
- // Search URL example.com/staging and root path to staging site /var/www/htdocs/staging
311
- $args['search_for'] = array(
312
- '//' . $this->sourceHostname,
313
- ABSPATH,
314
- '\/\/' . str_replace( '/', '\/', $this->sourceHostname ), // Used by revslider and several visual editors
315
- '%2F%2F' . str_replace( '/', '%2F', $this->sourceHostname ), // HTML entitity for WP Backery Page Builder Plugin
316
- //$this->getImagePathLive()
317
- );
318
-
319
-
320
- $args['replace_with'] = array(
321
- '//' . $this->destinationHostname,
322
- $this->options->destinationDir,
323
- '\/\/' . str_replace( '/', '\/', $this->destinationHostname ), // Used by revslider and several visual editors
324
- '%2F%2F' . str_replace( '/', '%2F', $this->destinationHostname ), // HTML entitity for WP Backery Page Builder Plugin
325
- //$this->getImagePathStaging()
326
- );
327
-
328
- $this->debugLog( "DB Search & Replace: Search: {$args['search_for'][0]}", \WPStaging\Utils\Logger::TYPE_INFO );
329
- $this->debugLog( "DB Search & Replace: Replace: {$args['replace_with'][0]}", \WPStaging\Utils\Logger::TYPE_INFO );
330
-
331
-
332
- $args['replace_guids'] = 'off';
333
- $args['dry_run'] = 'off';
334
- $args['case_insensitive'] = false;
335
- //$args['replace_mails'] = 'off';
336
- $args['skip_transients'] = 'on';
337
-
338
-
339
- // Allow filtering of search & replace parameters
340
- $args = apply_filters( 'wpstg_clone_searchreplace_params', $args );
341
-
342
- // Get a list of columns in this table.
343
- list( $primary_key, $columns ) = $this->get_columns( $table );
344
-
345
- // Bail out early if there isn't a primary key.
346
- // We commented this to search & replace through tables which have no primary keys like wp_revslider_slides
347
- // @todo test this carefully. If it causes (performance) issues we need to activate it again!
348
- // @since 2.4.4
349
- // if( null === $primary_key ) {
350
- // return false;
351
- // }
352
-
353
- $current_row = 0;
354
- $start = $this->options->job->start;
355
- $end = $this->settings->querySRLimit;
356
-
357
- // Grab the content of the table.
358
- $data = $this->stagingDb->get_results( "SELECT * FROM $table LIMIT $start, $end", ARRAY_A );
359
-
360
- // Filter certain rows (of other plugins)
361
- $filter = array(
362
- 'Admin_custome_login_Slidshow',
363
- 'Admin_custome_login_Social',
364
- 'Admin_custome_login_logo',
365
- 'Admin_custome_login_text',
366
- 'Admin_custome_login_login',
367
- 'Admin_custome_login_top',
368
- 'Admin_custome_login_dashboard',
369
- 'Admin_custome_login_Version',
370
- 'upload_path'
371
- );
372
-
373
- $filter = apply_filters( 'wpstg_clone_searchreplace_excl_rows', $filter );
374
-
375
- // Loop through the data.
376
- foreach ( $data as $row ) {
377
- $current_row++;
378
- $update_sql = array();
379
- $where_sql = array();
380
- $upd = false;
381
-
382
- // Skip rows below
383
- if( isset( $row['option_name'] ) && in_array( $row['option_name'], $filter ) ) {
384
- continue;
385
- }
386
-
387
- // Skip rows with transients (They can store huge data and we need to save memory)
388
- if( isset( $row['option_name'] ) && 'on' === $args['skip_transients'] && false !== strpos( $row['option_name'], '_transient' ) ) {
389
- continue;
390
- }
391
- // Skip rows with more than 5MB to save memory
392
- if( isset( $row['option_value'] ) && strlen( $row['option_value'] ) >= 5000000 ) {
393
- continue;
394
- }
395
-
396
-
397
- foreach ( $columns as $column ) {
398
-
399
- $dataRow = $row[$column];
400
-
401
-
402
- // Skip rows larger than 5MB
403
- $size = strlen( $dataRow );
404
- if( $size >= 5000000 ) {
405
- continue;
406
- }
407
-
408
- // Skip Primary key
409
- if( $column == $primary_key ) {
410
- $where_sql[] = $column . ' = "' . $this->mysql_escape_mimic( $dataRow ) . '"';
411
- continue;
412
- }
413
-
414
- // Skip GUIDs by default.
415
- if( 'on' !== $args['replace_guids'] && 'guid' == $column ) {
416
- continue;
417
- }
418
-
419
- // Skip mail addresses
420
- // if( 'off' === $args['replace_mails'] && false !== strpos( $dataRow, '@' . $this->multisiteDomainWithoutScheme ) ) {
421
- // continue;
422
- // }
423
- // Check options table
424
- if( $this->options->prefix . 'options' === $table ) {
425
-
426
- // Skip certain options
427
- // if( isset( $should_skip ) && true === $should_skip ) {
428
- // $should_skip = false;
429
- // continue;
430
- // }
431
- // Skip this row
432
- if( 'wpstg_existing_clones_beta' === $dataRow ||
433
- 'wpstg_existing_clones' === $dataRow ||
434
- 'wpstg_settings' === $dataRow ||
435
- 'wpstg_license_status' === $dataRow ||
436
- 'siteurl' === $dataRow ||
437
- 'home' === $dataRow
438
- ) {
439
- //$should_skip = true;
440
- continue;
441
- }
442
- }
443
-
444
- // Check the path delimiter for / or \/ and remove one of those which prevents from resulting in wrong syntax like domain.com/staging\/.
445
- // 1. local.wordpress.test -> local.wordpress.test/staging
446
- // 2. local.wordpress.test\/ -> local.wordpress.test\/staging\/
447
- $tmp = $args;
448
- if( false === strpos( $dataRow, $tmp['search_for'][0] ) ) {
449
- array_shift( $tmp['search_for'] ); // rtrim( $this->homeUrl, '/' ),
450
- array_shift( $tmp['replace_with'] ); // rtrim( $this->homeUrl, '/' ) . '/' . $this->options->cloneDirectoryName,
451
- } else {
452
- unset( $tmp['search_for'][1] );
453
- unset( $tmp['replace_with'][1] );
454
- // recount array
455
- $tmp['search_for'] = array_values( $tmp['search_for'] );
456
- $tmp['replace_with'] = array_values( $tmp['replace_with'] );
457
- }
458
-
459
- // Run a search replace on the data row and respect the serialisation.
460
- $i = 0;
461
- foreach ( $tmp['search_for'] as $replace ) {
462
- $dataRow = $this->recursive_unserialize_replace( $tmp['search_for'][$i], $tmp['replace_with'][$i], $dataRow, false, $args['case_insensitive'] );
463
- $i++;
464
- }
465
- unset( $replace );
466
- unset( $i );
467
- unset( $tmp );
468
-
469
- // Something was changed
470
- if( $row[$column] != $dataRow ) {
471
- $update_sql[] = $column . ' = "' . $this->mysql_escape_mimic( $dataRow ) . '"';
472
- $upd = true;
473
- }
474
- }
475
-
476
- // Determine what to do with updates.
477
- if( $args['dry_run'] === 'on' ) {
478
- // Don't do anything if a dry run
479
- } elseif( $upd && !empty( $where_sql ) ) {
480
- // If there are changes to make, run the query.
481
- $sql = 'UPDATE ' . $table . ' SET ' . implode( ', ', $update_sql ) . ' WHERE ' . implode( ' AND ', array_filter( $where_sql ) );
482
- $result = $this->stagingDb->query( $sql );
483
-
484
- if( !$result ) {
485
- $this->log( "Error updating row {$current_row} SQL: {$sql}", \WPStaging\Utils\Logger::TYPE_ERROR );
486
- }
487
- }
488
- } // end row loop
489
- unset( $row );
490
- unset( $update_sql );
491
- unset( $where_sql );
492
- unset( $sql );
493
- unset( $current_row );
494
-
495
-
496
- // DB Flush
497
- $this->stagingDb->flush();
498
- return true;
499
- }
500
-
501
- /**
502
- * Get path to multisite image folder e.g. wp-content/blogs.dir/ID/files or wp-content/uploads/sites/ID
503
- * @return string
504
- */
505
- private function getImagePathLive() {
506
- // Check first which structure is used
507
- $uploads = wp_upload_dir();
508
- $basedir = $uploads['basedir'];
509
- $blogId = get_current_blog_id();
510
-
511
- if( false === strpos( $basedir, 'blogs.dir' ) ) {
512
- // Since WP 3.5
513
- $path = $blogId > 1 ?
514
- 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'sites' . DIRECTORY_SEPARATOR . get_current_blog_id() . DIRECTORY_SEPARATOR :
515
- 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR;
516
- } else {
517
- // old blog structure
518
- $path = $blogId > 1 ?
519
- 'wp-content' . DIRECTORY_SEPARATOR . 'blogs.dir' . DIRECTORY_SEPARATOR . get_current_blog_id() . DIRECTORY_SEPARATOR . 'files' . DIRECTORY_SEPARATOR :
520
- 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR;
521
- }
522
- return $path;
523
- }
524
-
525
- /**
526
- * Get path to staging site image path wp-content/uploads
527
- * @return string
528
- */
529
- private function getImagePathStaging() {
530
- return 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR;
531
- }
532
-
533
- /**
534
- * Adapted from interconnect/it's search/replace script.
535
- *
536
- * @link https://interconnectit.com/products/search-and-replace-for-wordpress-databases/
537
- *
538
- * Take a serialised array and unserialise it replacing elements as needed and
539
- * unserialising any subordinate arrays and performing the replace on those too.
540
- *
541
- * @access private
542
- * @param string $from String we're looking to replace.
543
- * @param string $to What we want it to be replaced with
544
- * @param array $data Used to pass any subordinate arrays back to in.
545
- * @param boolean $serialized Does the array passed via $data need serialising.
546
- * @param sting|boolean $case_insensitive Set to 'on' if we should ignore case, false otherwise.
547
- *
548
- * @return string|array The original array with all elements replaced as needed.
549
- */
550
- private function recursive_unserialize_replace( $from = '', $to = '', $data = '', $serialized = false, $case_insensitive = false ) {
551
- try {
552
- // PDO instances can not be serialized or unserialized
553
- if( is_serialized( $data ) && strpos( $data, 'O:3:"PDO":0:' ) !== false ) {
554
- return $data;
555
- }
556
- // Some unserialized data cannot be re-serialized eg. SimpleXMLElements
557
- if( is_serialized( $data ) && ( $unserialized = @unserialize( $data ) ) !== false ) {
558
- $data = $this->recursive_unserialize_replace( $from, $to, $unserialized, true, $case_insensitive );
559
- } elseif( is_array( $data ) ) {
560
- $tmp = array();
561
- foreach ( $data as $key => $value ) {
562
- $tmp[$key] = $this->recursive_unserialize_replace( $from, $to, $value, false, $case_insensitive );
563
- }
564
-
565
- $data = $tmp;
566
- unset( $tmp );
567
- } elseif( is_object( $data ) ) {
568
- $props = get_object_vars( $data );
569
-
570
- // Do a search & replace
571
- if( empty( $props['__PHP_Incomplete_Class_Name'] ) ) {
572
- $tmp = $data;
573
- foreach ( $props as $key => $value ) {
574
- if( $key === '' || ord( $key[0] ) === 0 ) {
575
- continue;
576
- }
577
- $tmp->$key = $this->recursive_unserialize_replace( $from, $to, $value, false, $case_insensitive );
578
- }
579
- $data = $tmp;
580
- $tmp = '';
581
- $props = '';
582
- unset( $tmp );
583
- unset( $props );
584
- }
585
- } else {
586
- if( is_string( $data ) ) {
587
- if( !empty( $from ) && !empty( $to ) ) {
588
- $data = $this->str_replace( $from, $to, $data, $case_insensitive );
589
- }
590
- }
591
- }
592
-
593
- if( $serialized ) {
594
- return serialize( $data );
595
- }
596
- } catch ( Exception $error ) {
597
-
598
- }
599
-
600
- return $data;
601
- }
602
-
603
- /**
604
- * Check if the object is a valid one and not __PHP_Incomplete_Class_Name
605
- * Can not use is_object alone because in php 7.2 it's returning true even though object is __PHP_Incomplete_Class_Name
606
- * @return boolean
607
- */
608
- // private function isValidObject( $data ) {
609
- // if( !is_object( $data ) || gettype( $data ) != 'object' ) {
610
- // return false;
611
- // }
612
- //
613
- // $invalid_class_props = get_object_vars( $data );
614
- //
615
- // if (!isset($invalid_class_props['__PHP_Incomplete_Class_Name'])){
616
- // // Assume it must be an valid object
617
- // return true;
618
- // }
619
- //
620
- // $invalid_object_class = $invalid_class_props['__PHP_Incomplete_Class_Name'];
621
- //
622
- // if( !empty( $invalid_object_class ) ) {
623
- // return false;
624
- // }
625
- //
626
- // // Assume it must be an valid object
627
- // return true;
628
- // }
629
-
630
- /**
631
- * Mimics the mysql_real_escape_string function. Adapted from a post by 'feedr' on php.net.
632
- * @link http://php.net/manual/en/function.mysql-real-escape-string.php#101248
633
- * @access public
634
- * @param string $input The string to escape.
635
- * @return string
636
- */
637
- private function mysql_escape_mimic( $input ) {
638
- if( is_array( $input ) ) {
639
- return array_map( __METHOD__, $input );
640
- }
641
- if( !empty( $input ) && is_string( $input ) ) {
642
- return str_replace( array('\\', "\0", "\n", "\r", "'", '"', "\x1a"), array('\\\\', '\\0', '\\n', '\\r', "\\'", '\\"', '\\Z'), $input );
643
- }
644
-
645
- return $input;
646
- }
647
-
648
- /**
649
- * Return unserialized object or array
650
- *
651
- * @param string $serialized_string Serialized string.
652
- * @param string $method The name of the caller method.
653
- *
654
- * @return mixed, false on failure
655
- */
656
- private static function unserialize( $serialized_string ) {
657
- if( !is_serialized( $serialized_string ) ) {
658
- return false;
659
- }
660
-
661
- $serialized_string = trim( $serialized_string );
662
- $unserialized_string = @unserialize( $serialized_string );
663
-
664
- return $unserialized_string;
665
- }
666
-
667
- /**
668
- * Wrapper for str_replace
669
- *
670
- * @param string $from
671
- * @param string $to
672
- * @param string $data
673
- * @param string|bool $case_insensitive
674
- *
675
- * @return string
676
- */
677
- private function str_replace( $from, $to, $data, $case_insensitive = false ) {
678
-
679
- // Add filter
680
- $excludes = apply_filters( 'wpstg_clone_searchreplace_excl', array() );
681
-
682
- // Build pattern
683
- $regexExclude = '';
684
- foreach ( $excludes as $exclude ) {
685
- $regexExclude .= $exclude . '(*SKIP)(FAIL)|';
686
- }
687
-
688
- if( 'on' === $case_insensitive ) {
689
- //$data = str_ireplace( $from, $to, $data );
690
- $data = preg_replace( '#' . $regexExclude . preg_quote( $from ) . '#i', $to, $data );
691
- } else {
692
- //$data = str_replace( $from, $to, $data );
693
- $data = preg_replace( '#' . $regexExclude . preg_quote( $from ) . '#', $to, $data );
694
- }
695
-
696
- return $data;
697
- }
698
-
699
- /**
700
- * Set the job
701
- * @param string $table
702
- */
703
- private function setJob( $table ) {
704
- if( !empty( $this->options->job->current ) ) {
705
- return;
706
- }
707
-
708
- $this->options->job->current = $table;
709
- $this->options->job->start = 0;
710
- }
711
-
712
- /**
713
- * Start Job
714
- * @param string $new
715
- * @param string $old
716
- * @return bool
717
- */
718
- private function startJob( $new, $old ) {
719
-
720
- if( $this->isExcludedTable( $new ) ) {
721
- return false;
722
- }
723
-
724
- // Table does not exist
725
- $result = $this->productionDb->query( "SHOW TABLES LIKE '{$old}'" );
726
- if( !$result || 0 === $result ) {
727
- return false;
728
- }
729
-
730
- if( 0 != $this->options->job->start ) {
731
- return true;
732
- }
733
-
734
- $this->options->job->total = ( int ) $this->productionDb->get_var( "SELECT COUNT(1) FROM {$old}" );
735
-
736
- if( 0 == $this->options->job->total ) {
737
- $this->finishStep();
738
- return false;
739
- }
740
-
741
- return true;
742
- }
743
-
744
- /**
745
- * Is table excluded from search replace processing?
746
- * @param string $table
747
- * @return boolean
748
- */
749
- private function isExcludedTable( $table ) {
750
-
751
- $customTables = apply_filters( 'wpstg_clone_searchreplace_tables_exclude', array() );
752
- $defaultTables = array('blogs');
753
-
754
- $tables = array_merge( $customTables, $defaultTables );
755
-
756
- $excludedTables = array();
757
- foreach ( $tables as $key => $value ) {
758
- $excludedTables[] = $this->options->prefix . $value;
759
- }
760
-
761
- if( in_array( $table, $excludedTables ) ) {
762
- return true;
763
- }
764
- return false;
765
- }
766
-
767
- /**
768
- * Finish the step
769
- */
770
- private function finishStep() {
771
- // This job is not finished yet
772
- if( $this->options->job->total > $this->options->job->start ) {
773
- return false;
774
- }
775
-
776
- // Add it to cloned tables listing
777
- $this->options->clonedTables[] = $this->options->tables[$this->options->currentStep];
778
-
779
- // Reset job
780
- $this->options->job = new \stdClass();
781
-
782
- return true;
783
- }
784
-
785
- /**
786
- * Drop table if necessary
787
- * @param string $new
788
- */
789
- private function dropTable( $new ) {
790
- $old = $this->stagingDb->get_var( $this->stagingDb->prepare( "SHOW TABLES LIKE %s", $new ) );
791
-
792
- if( !$this->shouldDropTable( $new, $old ) ) {
793
- return;
794
- }
795
-
796
- $this->log( "DB Search & Replace: {$new} already exists, dropping it first" );
797
- $this->stagingDb->query( "DROP TABLE {$new}" );
798
- }
799
-
800
- /**
801
- * Check if table needs to be dropped
802
- * @param string $new
803
- * @param string $old
804
- * @return bool
805
- */
806
- private function shouldDropTable( $new, $old ) {
807
- return (
808
- $old == $new &&
809
- (
810
- !isset( $this->options->job->current ) ||
811
- !isset( $this->options->job->start ) ||
812
- 0 == $this->options->job->start
813
- )
814
- );
815
- }
816
-
817
- /**
818
- * Check if WP is installed in subdir
819
- * @return boolean
820
- */
821
- private function isSubDir() {
822
- // Compare names without scheme to bypass cases where siteurl and home have different schemes http / https
823
- // This is happening much more often than you would expect
824
- $siteurl = preg_replace( '#^https?://#', '', rtrim( get_option( 'siteurl' ), '/' ) );
825
- $home = preg_replace( '#^https?://#', '', rtrim( get_option( 'home' ), '/' ) );
826
-
827
- if( $home !== $siteurl ) {
828
- return true;
829
- }
830
- return false;
831
- }
832
-
833
- }
1
+ <?php
2
+
3
+ namespace WPStaging\Backend\Modules\Jobs\Multisite;
4
+
5
+ // No Direct Access
6
+ if( !defined( "WPINC" ) ) {
7
+ die;
8
+ }
9
+
10
+ use WPStaging\WPStaging;
11
+ use WPStaging\Utils\Strings;
12
+ use WPStaging\Backend\Modules\Jobs\JobExecutable;
13
+
14
+ /**
15
+ * Class Database
16
+ * @package WPStaging\Backend\Modules\Jobs
17
+ */
18
+ class SearchReplaceExternal extends JobExecutable {
19
+
20
+ /**
21
+ * @var int
22
+ */
23
+ private $total = 0;
24
+
25
+ /**
26
+ * Staging Site DB
27
+ * @var \WPDB
28
+ */
29
+ private $stagingDb;
30
+
31
+ /**
32
+ * Production Site DB
33
+ * @var \WPDB
34
+ */
35
+ private $productionDb;
36
+
37
+ /**
38
+ *
39
+ * @var string
40
+ */
41
+ private $sourceHostname;
42
+
43
+ /**
44
+ *
45
+ * @var string
46
+ */
47
+ private $destinationHostname;
48
+
49
+ /**
50
+ *
51
+ * @var Obj
52
+ */
53
+ private $strings;
54
+
55
+ /**
56
+ * The prefix of the new database tables which are used for the live site after updating tables
57
+ * @var string
58
+ */
59
+ public $tmpPrefix;
60
+
61
+ /**
62
+ * Initialize
63
+ */
64
+ public function initialize() {
65
+ $this->total = count( $this->options->tables );
66
+ $this->stagingDb = $this->getStagingDB();
67
+ $this->productionDb = WPStaging::getInstance()->get( "wpdb" );
68
+ $this->tmpPrefix = $this->options->prefix;
69
+ $this->strings = new Strings();
70
+ $this->sourceHostname = $this->getSourceHostname();
71
+ $this->destinationHostname = $this->getDestinationHostname();
72
+ }
73
+
74
+ /**
75
+ * Get database object to interact with
76
+ */
77
+ private function getStagingDB() {
78
+ return new \wpdb( $this->options->databaseUser, $this->options->databasePassword, $this->options->databaseDatabase, $this->options->databaseServer );
79
+ }
80
+
81
+ public function start() {
82
+ // Skip job. Nothing to do
83
+ if( $this->options->totalSteps === 0 ) {
84
+ $this->prepareResponse( true, false );
85
+ }
86
+
87
+ $this->run();
88
+
89
+ // Save option, progress
90
+ $this->saveOptions();
91
+
92
+ return ( object ) $this->response;
93
+ }
94
+
95
+ /**
96
+ * Calculate Total Steps in This Job and Assign It to $this->options->totalSteps
97
+ * @return void
98
+ */
99
+ protected function calculateTotalSteps() {
100
+ $this->options->totalSteps = $this->total;
101
+ }
102
+
103
+ /**
104
+ * Execute the Current Step
105
+ * Returns false when over threshold limits are hit or when the job is done, true otherwise
106
+ * @return bool
107
+ */
108
+ protected function execute() {
109
+ // Over limits threshold
110
+ if( $this->isOverThreshold() ) {
111
+ // Prepare response and save current progress
112
+ $this->prepareResponse( false, false );
113
+ $this->saveOptions();
114
+ return false;
115
+ }
116
+
117
+ // No more steps, finished
118
+ if( $this->options->currentStep > $this->total || !isset( $this->options->tables[$this->options->currentStep] ) ) {
119
+ $this->prepareResponse( true, false );
120
+ return false;
121
+ }
122
+
123
+ // Table is excluded
124
+ if( in_array( $this->options->tables[$this->options->currentStep], $this->options->excludedTables ) ) {
125
+ $this->prepareResponse();
126
+ return true;
127
+ }
128
+
129
+ // Search & Replace
130
+ if( !$this->stopExecution() && !$this->updateTable( $this->options->tables[$this->options->currentStep] ) ) {
131
+ // Prepare Response
132
+ $this->prepareResponse( false, false );
133
+
134
+ // Not finished
135
+ return true;
136
+ }
137
+
138
+
139
+ // Prepare Response
140
+ $this->prepareResponse();
141
+
142
+ // Not finished
143
+ return true;
144
+ }
145
+
146
+ /**
147
+ * Stop Execution immediately
148
+ * return mixed bool | json
149
+ */
150
+ private function stopExecution() {
151
+ // if( $this->stagingDb->prefix == $this->tmpPrefix ) {
152
+ // $this->returnException( 'Fatal Error 9: Prefix ' . $this->stagingDb->prefix . ' is used for the live site hence it can not be used for the staging site as well. Please ask support@wp-staging.com how to resolve this.' );
153
+ // }
154
+ return false;
155
+ }
156
+
157
+ /**
158
+ * Copy Tables
159
+ * @param string $tableName
160
+ * @return bool
161
+ */
162
+ private function updateTable( $tableName ) {
163
+ $strings = new Strings();
164
+ $table = $strings->str_replace_first( $this->productionDb->prefix, '', $tableName );
165
+ $newTableName = $this->tmpPrefix . $table;
166
+
167
+ // Save current job
168
+ $this->setJob( $newTableName );
169
+
170
+ // Beginning of the job
171
+ if( !$this->startJob( $newTableName, $tableName ) ) {
172
+ return true;
173
+ }
174
+ // Copy data
175
+ $this->startReplace( $newTableName );
176
+
177
+ // Finish the step
178
+ return $this->finishStep();
179
+ }
180
+
181
+ /**
182
+ * Get source Hostname depending on wheather WP has been installed in sub dir or not
183
+ * @return type
184
+ */
185
+ private function getSourceHostname() {
186
+
187
+ if( $this->isSubDir() ) {
188
+ return trailingslashit( $this->multisiteHomeUrlWithoutScheme ) . '/' . $this->getSubDir();
189
+ }
190
+ return $this->multisiteHomeUrlWithoutScheme;
191
+ }
192
+
193
+ /**
194
+ * Get destination Hostname depending on wheather WP has been installed in sub dir or not
195
+ * Retun host name without scheme
196
+ * @return type
197
+ */
198
+ private function getDestinationHostname() {
199
+
200
+ if( !empty( $this->options->cloneHostname ) ) {
201
+ return $this->strings->getUrlWithoutScheme( $this->options->cloneHostname );
202
+ }
203
+
204
+ if( $this->isSubDir() ) {
205
+ return trailingslashit( $this->strings->getUrlWithoutScheme( $this->multisiteDomainWithoutScheme ) ) . $this->getSubDir() . '/' . $this->options->cloneDirectoryName;
206
+ }
207
+ return trailingslashit( $this->strings->getUrlWithoutScheme( $this->multisiteDomainWithoutScheme ) ) . $this->options->cloneDirectoryName;
208
+ }
209
+
210
+ /**
211
+ * Get the install sub directory if WP is installed in sub directory
212
+ * @return string
213
+ */
214
+ private function getSubDir() {
215
+ $home = get_option( 'home' );
216
+ $siteurl = get_option( 'siteurl' );
217
+
218
+ if( empty( $home ) || empty( $siteurl ) ) {
219
+ return '';
220
+ }
221
+
222
+ $dir = str_replace( $home, '', $siteurl );
223
+ return str_replace( '/', '', $dir );
224
+ }
225
+
226
+ /**
227
+ * Start search replace job
228
+ * @param string $new
229
+ * @param string $old
230
+ */
231
+ private function startReplace( $table ) {
232
+ $rows = $this->options->job->start + $this->settings->querySRLimit;
233
+ $this->log(
234
+ "DB Search & Replace: Table {$table} {$this->options->job->start} to {$rows} records"
235
+ );
236
+
237
+ // Search & Replace
238
+ $this->searchReplace( $table, $rows, array() );
239
+
240
+ // Set new offset
241
+ $this->options->job->start += $this->settings->querySRLimit;
242
+ }
243
+
244
+ /**
245
+ * Returns the number of pages in a table.
246
+ * @access public
247
+ * @return int
248
+ */
249
+ // private function get_pages_in_table( $table ) {
250
+ //
251
+ // // Table does not exist
252
+ // $table = str_replace( $this->options->prefix . '.', null, $table );
253
+ // $result = $this->productionDb->query( "SHOW TABLES LIKE '{$table}'" );
254
+ // if( !$result || 0 === $result ) {
255
+ // return 0;
256
+ // }
257
+ //
258
+ // $table = esc_sql( $table );
259
+ // $rows = $this->productionDb->get_var( "SELECT COUNT(*) FROM $table" );
260
+ // $pages = ceil( $rows / $this->settings->querySRLimit );
261
+ // return absint( $pages );
262
+ // }
263
+
264
+ /**
265
+ * Gets the columns in a table.
266
+ * @access public
267
+ * @param string $table The table to check.
268
+ * @return array
269
+ */
270
+ private function get_columns( $table ) {
271
+ $primary_key = null;
272
+ $columns = array();
273
+ $fields = $this->stagingDb->get_results( 'DESCRIBE ' . $table );
274
+ if( is_array( $fields ) ) {
275
+ foreach ( $fields as $column ) {
276
+ $columns[] = $column->Field;
277
+ if( $column->Key == 'PRI' ) {
278
+ $primary_key = $column->Field;
279
+ }
280
+ }
281
+ }
282
+ return array($primary_key, $columns);
283
+ }
284
+
285
+ /**
286
+ * Adapated from interconnect/it's search/replace script, adapted from Better Search Replace
287
+ *
288
+ * Modified to use WordPress wpdb functions instead of PHP's native mysql/pdo functions,
289
+ * and to be compatible with batch processing.
290
+ *
291
+ * @link https://interconnectit.com/products/search-and-replace-for-wordpress-databases/
292
+ *
293
+ * @access public
294
+ * @param string $table The table to run the replacement on.
295
+ * @param int $page The page/block to begin the query on.
296
+ * @param array $args An associative array containing arguments for this run.
297
+ * @return array
298
+ */
299
+ private function searchReplace( $table, $page, $args ) {
300
+
301
+ if( $this->thirdParty->isSearchReplaceExcluded( $table ) ) {
302
+ $this->log( "DB Search & Replace: Skip {$table}", \WPStaging\Utils\Logger::TYPE_INFO );
303
+ return true;
304
+ }
305
+
306
+ // Load up the default settings for this chunk.
307
+ $table = esc_sql( $table );
308
+ //$current_page = $this->options->job->start + $this->settings->querySRLimit;
309
+ //$pages = $this->get_pages_in_table( $table );
310
+ // Search URL example.com/staging and root path to staging site /var/www/htdocs/staging
311
+ $args['search_for'] = array(
312
+ '//' . $this->sourceHostname,
313
+ ABSPATH,
314
+ '\/\/' . str_replace( '/', '\/', $this->sourceHostname ), // Used by revslider and several visual editors
315
+ '%2F%2F' . str_replace( '/', '%2F', $this->sourceHostname ), // HTML entitity for WP Backery Page Builder Plugin
316
+ //$this->getImagePathLive()
317
+ );
318
+
319
+
320
+ $args['replace_with'] = array(
321
+ '//' . $this->destinationHostname,
322
+ $this->options->destinationDir,
323
+ '\/\/' . str_replace( '/', '\/', $this->destinationHostname ), // Used by revslider and several visual editors
324
+ '%2F%2F' . str_replace( '/', '%2F', $this->destinationHostname ), // HTML entitity for WP Backery Page Builder Plugin
325
+ //$this->getImagePathStaging()
326
+ );
327
+
328
+ $this->debugLog( "DB Search & Replace: Search: {$args['search_for'][0]}", \WPStaging\Utils\Logger::TYPE_INFO );
329
+ $this->debugLog( "DB Search & Replace: Replace: {$args['replace_with'][0]}", \WPStaging\Utils\Logger::TYPE_INFO );
330
+
331
+
332
+ $args['replace_guids'] = 'off';
333
+ $args['dry_run'] = 'off';
334
+ $args['case_insensitive'] = false;
335
+ //$args['replace_mails'] = 'off';
336
+ $args['skip_transients'] = 'on';
337
+
338
+
339
+ // Allow filtering of search & replace parameters
340
+ $args = apply_filters( 'wpstg_clone_searchreplace_params', $args );
341
+
342
+ // Get a list of columns in this table.
343
+ list( $primary_key, $columns ) = $this->get_columns( $table );
344
+
345
+ // Bail out early if there isn't a primary key.
346
+ // We commented this to search & replace through tables which have no primary keys like wp_revslider_slides
347
+ // @todo test this carefully. If it causes (performance) issues we need to activate it again!
348
+ // @since 2.4.4
349
+ // if( null === $primary_key ) {
350
+ // return false;
351
+ // }
352
+
353
+ $current_row = 0;
354
+ $start = $this->options->job->start;
355
+ $end = $this->settings->querySRLimit;
356
+
357
+ // Grab the content of the table.
358
+ $data = $this->stagingDb->get_results( "SELECT * FROM $table LIMIT $start, $end", ARRAY_A );
359
+
360
+ // Filter certain rows (of other plugins)
361
+ $filter = array(
362
+ 'Admin_custome_login_Slidshow',
363
+ 'Admin_custome_login_Social',
364
+ 'Admin_custome_login_logo',
365
+ 'Admin_custome_login_text',
366
+ 'Admin_custome_login_login',
367
+ 'Admin_custome_login_top',
368
+ 'Admin_custome_login_dashboard',
369
+ 'Admin_custome_login_Version',
370
+ 'upload_path'
371
+ );
372
+
373
+ $filter = apply_filters( 'wpstg_clone_searchreplace_excl_rows', $filter );
374
+
375
+ // Loop through the data.
376
+ foreach ( $data as $row ) {
377
+ $current_row++;
378
+ $update_sql = array();
379
+ $where_sql = array();
380
+ $upd = false;
381
+
382
+ // Skip rows below
383
+ if( isset( $row['option_name'] ) && in_array( $row['option_name'], $filter ) ) {
384
+ continue;
385
+ }
386
+
387
+ // Skip rows with transients (They can store huge data and we need to save memory)
388
+ if( isset( $row['option_name'] ) && 'on' === $args['skip_transients'] && false !== strpos( $row['option_name'], '_transient' ) ) {
389
+ continue;
390
+ }
391
+ // Skip rows with more than 5MB to save memory
392
+ if( isset( $row['option_value'] ) && strlen( $row['option_value'] ) >= 5000000 ) {
393
+ continue;
394
+ }
395
+
396
+
397
+ foreach ( $columns as $column ) {
398
+
399
+ $dataRow = $row[$column];
400
+
401
+
402
+ // Skip rows larger than 5MB
403
+ $size = strlen( $dataRow );
404
+ if( $size >= 5000000 ) {
405
+ continue;
406
+ }
407
+
408
+ // Skip Primary key
409
+ if( $column == $primary_key ) {
410
+ $where_sql[] = $column . ' = "' . $this->mysql_escape_mimic( $dataRow ) . '"';
411
+ continue;
412
+ }
413
+
414
+ // Skip GUIDs by default.
415
+ if( 'on' !== $args['replace_guids'] && 'guid' == $column ) {
416
+ continue;
417
+ }
418
+
419
+ // Skip mail addresses
420
+ // if( 'off' === $args['replace_mails'] && false !== strpos( $dataRow, '@' . $this->multisiteDomainWithoutScheme ) ) {
421
+ // continue;
422
+ // }
423
+ // Check options table
424
+ if( $this->options->prefix . 'options' === $table ) {
425
+
426
+ // Skip certain options
427
+ // if( isset( $should_skip ) && true === $should_skip ) {
428
+ // $should_skip = false;
429
+ // continue;
430
+ // }
431
+ // Skip this row
432
+ if( 'wpstg_existing_clones_beta' === $dataRow ||
433
+ 'wpstg_existing_clones' === $dataRow ||
434
+ 'wpstg_settings' === $dataRow ||
435
+ 'wpstg_license_status' === $dataRow ||
436
+ 'siteurl' === $dataRow ||
437
+ 'home' === $dataRow
438
+ ) {
439
+ //$should_skip = true;
440
+ continue;
441
+ }
442
+ }
443
+
444
+ // Check the path delimiter for / or \/ and remove one of those which prevents from resulting in wrong syntax like domain.com/staging\/.
445
+ // 1. local.wordpress.test -> local.wordpress.test/staging
446
+ // 2. local.wordpress.test\/ -> local.wordpress.test\/staging\/
447
+ $tmp = $args;
448
+ if( false === strpos( $dataRow, $tmp['search_for'][0] ) ) {
449
+ array_shift( $tmp['search_for'] ); // rtrim( $this->homeUrl, '/' ),
450
+ array_shift( $tmp['replace_with'] ); // rtrim( $this->homeUrl, '/' ) . '/' . $this->options->cloneDirectoryName,
451
+ } else {
452
+ unset( $tmp['search_for'][1] );
453
+ unset( $tmp['replace_with'][1] );
454
+ // recount array
455
+ $tmp['search_for'] = array_values( $tmp['search_for'] );
456
+ $tmp['replace_with'] = array_values( $tmp['replace_with'] );
457
+ }
458
+
459
+ // Run a search replace on the data row and respect the serialisation.
460
+ $i = 0;
461
+ foreach ( $tmp['search_for'] as $replace ) {
462
+ $dataRow = $this->recursive_unserialize_replace( $tmp['search_for'][$i], $tmp['replace_with'][$i], $dataRow, false, $args['case_insensitive'] );
463
+ $i++;
464
+ }
465
+ unset( $replace );
466
+ unset( $i );
467
+ unset( $tmp );
468
+
469
+ // Something was changed
470
+ if( $row[$column] != $dataRow ) {
471
+ $update_sql[] = $column . ' = "' . $this->mysql_escape_mimic( $dataRow ) . '"';
472
+ $upd = true;
473
+ }
474
+ }
475
+
476
+ // Determine what to do with updates.
477
+ if( $args['dry_run'] === 'on' ) {
478
+ // Don't do anything if a dry run
479
+ } elseif( $upd && !empty( $where_sql ) ) {
480
+ // If there are changes to make, run the query.
481
+ $sql = 'UPDATE ' . $table . ' SET ' . implode( ', ', $update_sql ) . ' WHERE ' . implode( ' AND ', array_filter( $where_sql ) );
482
+ $result = $this->stagingDb->query( $sql );
483
+
484
+ if( !$result ) {
485
+ $this->log( "Error updating row {$current_row} SQL: {$sql}", \WPStaging\Utils\Logger::TYPE_ERROR );
486
+ }
487
+ }
488
+ } // end row loop
489
+ unset( $row );
490
+ unset( $update_sql );
491
+ unset( $where_sql );
492
+ unset( $sql );
493
+ unset( $current_row );
494
+
495
+
496
+ // DB Flush
497
+ $this->stagingDb->flush();
498
+ return true;
499
+ }
500
+
501
+ /**
502
+ * Get path to multisite image folder e.g. wp-content/blogs.dir/ID/files or wp-content/uploads/sites/ID
503
+ * @return string
504
+ */
505
+ private function getImagePathLive() {
506
+ // Check first which structure is used
507
+ $uploads = wp_upload_dir();
508
+ $basedir = $uploads['basedir'];
509
+ $blogId = get_current_blog_id();
510
+
511
+ if( false === strpos( $basedir, 'blogs.dir' ) ) {
512
+ // Since WP 3.5
513
+ $path = $blogId > 1 ?
514
+ 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'sites' . DIRECTORY_SEPARATOR . get_current_blog_id() . DIRECTORY_SEPARATOR :
515
+ 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR;
516
+ } else {
517
+ // old blog structure
518
+ $path = $blogId > 1 ?
519
+ 'wp-content' . DIRECTORY_SEPARATOR . 'blogs.dir' . DIRECTORY_SEPARATOR . get_current_blog_id() . DIRECTORY_SEPARATOR . 'files' . DIRECTORY_SEPARATOR :
520
+ 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR;
521
+ }
522
+ return $path;
523
+ }
524
+
525
+ /**
526
+ * Get path to staging site image path wp-content/uploads
527
+ * @return string
528
+ */
529
+ private function getImagePathStaging() {
530
+ return 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR;
531
+ }
532
+
533
+ /**
534
+ * Adapted from interconnect/it's search/replace script.
535
+ *
536
+ * @link https://interconnectit.com/products/search-and-replace-for-wordpress-databases/
537
+ *
538
+ * Take a serialised array and unserialise it replacing elements as needed and
539
+ * unserialising any subordinate arrays and performing the replace on those too.
540
+ *
541
+ * @access private
542
+ * @param string $from String we're looking to replace.
543
+ * @param string $to What we want it to be replaced with
544
+ * @param array $data Used to pass any subordinate arrays back to in.
545
+ * @param boolean $serialized Does the array passed via $data need serialising.
546
+ * @param sting|boolean $case_insensitive Set to 'on' if we should ignore case, false otherwise.
547
+ *
548
+ * @return string|array The original array with all elements replaced as needed.
549
+ */
550
+ private function recursive_unserialize_replace( $from = '', $to = '', $data = '', $serialized = false, $case_insensitive = false ) {
551
+ try {
552
+ // PDO instances can not be serialized or unserialized
553
+ if( is_serialized( $data ) && strpos( $data, 'O:3:"PDO":0:' ) !== false ) {
554
+ return $data;
555
+ }
556
+ // Some unserialized data cannot be re-serialized eg. SimpleXMLElements
557
+ if( is_serialized( $data ) && ( $unserialized = @unserialize( $data ) ) !== false ) {
558
+ $data = $this->recursive_unserialize_replace( $from, $to, $unserialized, true, $case_insensitive );
559
+ } elseif( is_array( $data ) ) {
560
+ $tmp = array();
561
+ foreach ( $data as $key => $value ) {
562
+ $tmp[$key] = $this->recursive_unserialize_replace( $from, $to, $value, false, $case_insensitive );
563
+ }
564
+
565
+ $data = $tmp;
566
+ unset( $tmp );
567
+ } elseif( is_object( $data ) ) {
568
+ $props = get_object_vars( $data );
569
+
570
+ // Do a search & replace
571
+ if( empty( $props['__PHP_Incomplete_Class_Name'] ) ) {
572
+ $tmp = $data;
573
+ foreach ( $props as $key => $value ) {
574
+ if( $key === '' || ord( $key[0] ) === 0 ) {
575
+ continue;
576
+ }
577
+ $tmp->$key = $this->recursive_unserialize_replace( $from, $to, $value, false, $case_insensitive );
578
+ }
579
+ $data = $tmp;
580
+ $tmp = '';
581
+ $props = '';
582
+ unset( $tmp );
583
+ unset( $props );
584
+ }
585
+ } else {
586
+ if( is_string( $data ) ) {
587
+ if( !empty( $from ) && !empty( $to ) ) {
588
+ $data = $this->str_replace( $from, $to, $data, $case_insensitive );
589
+ }
590
+ }
591
+ }
592
+
593
+ if( $serialized ) {
594
+ return serialize( $data );
595
+ }
596
+ } catch ( Exception $error ) {
597
+
598
+ }
599
+
600
+ return $data;
601
+ }
602
+
603
+ /**
604
+ * Check if the object is a valid one and not __PHP_Incomplete_Class_Name
605
+ * Can not use is_object alone because in php 7.2 it's returning true even though object is __PHP_Incomplete_Class_Name
606
+ * @return boolean
607
+ */
608
+ // private function isValidObject( $data ) {
609
+ // if( !is_object( $data ) || gettype( $data ) != 'object' ) {
610
+ // return false;
611
+ // }
612
+ //
613
+ // $invalid_class_props = get_object_vars( $data );
614
+ //
615
+ // if (!isset($invalid_class_props['__PHP_Incomplete_Class_Name'])){
616
+ // // Assume it must be an valid object
617
+ // return true;
618
+ // }
619
+ //
620
+ // $invalid_object_class = $invalid_class_props['__PHP_Incomplete_Class_Name'];
621
+ //
622
+ // if( !empty( $invalid_object_class ) ) {
623
+ // return false;
624
+ // }
625
+ //
626
+ // // Assume it must be an valid object
627
+ // return true;
628
+ // }
629
+
630
+ /**
631
+ * Mimics the mysql_real_escape_string function. Adapted from a post by 'feedr' on php.net.
632
+ * @link http://php.net/manual/en/function.mysql-real-escape-string.php#101248
633
+ * @access public
634
+ * @param string $input The string to escape.
635
+ * @return string
636
+ */
637
+ private function mysql_escape_mimic( $input ) {
638
+ if( is_array( $input ) ) {
639
+ return array_map( __METHOD__, $input );
640
+ }
641
+ if( !empty( $input ) && is_string( $input ) ) {
642
+ return str_replace( array('\\', "\0", "\n", "\r", "'", '"', "\x1a"), array('\\\\', '\\0', '\\n', '\\r', "\\'", '\\"', '\\Z'), $input );
643
+ }
644
+
645
+ return $input;
646
+ }
647
+
648
+ /**
649
+ * Return unserialized object or array
650
+ *
651
+ * @param string $serialized_string Serialized string.
652
+ * @param string $method The name of the caller method.
653
+ *
654
+ * @return mixed, false on failure
655
+ */
656
+ private static function unserialize( $serialized_string ) {
657
+ if( !is_serialized( $serialized_string ) ) {
658
+ return false;
659
+ }
660
+
661
+ $serialized_string = trim( $serialized_string );
662
+ $unserialized_string = @unserialize( $serialized_string );
663
+
664
+ return $unserialized_string;
665
+ }
666
+
667
+ /**
668
+ * Wrapper for str_replace
669
+ *
670
+ * @param string $from
671
+ * @param string $to
672
+ * @param string $data
673
+ * @param string|bool $case_insensitive
674
+ *
675
+ * @return string
676
+ */
677
+ private function str_replace( $from, $to, $data, $case_insensitive = false ) {
678
+
679
+ // Add filter
680
+ $excludes = apply_filters( 'wpstg_clone_searchreplace_excl', array() );
681
+
682
+ // Build pattern
683
+ $regexExclude = '';
684
+ foreach ( $excludes as $exclude ) {
685
+ $regexExclude .= $exclude . '(*SKIP)(FAIL)|';
686
+ }
687
+
688
+ if( 'on' === $case_insensitive ) {
689
+ //$data = str_ireplace( $from, $to, $data );
690
+ $data = preg_replace( '#' . $regexExclude . preg_quote( $from ) . '#i', $to, $data );
691
+ } else {
692
+ //$data = str_replace( $from, $to, $data );
693
+ $data = preg_replace( '#' . $regexExclude . preg_quote( $from ) . '#', $to, $data );
694
+ }
695
+
696
+ return $data;
697
+ }
698
+
699
+ /**
700
+ * Set the job
701
+ * @param string $table
702
+ */
703
+ private function setJob( $table ) {
704
+ if( !empty( $this->options->job->current ) ) {
705
+ return;
706
+ }
707
+
708
+ $this->options->job->current = $table;
709
+ $this->options->job->start = 0;
710
+ }
711
+
712
+ /**
713
+ * Start Job
714
+ * @param string $new
715
+ * @param string $old
716
+ * @return bool
717
+ */
718
+ private function startJob( $new, $old ) {
719
+
720
+ if( $this->isExcludedTable( $new ) ) {
721
+ return false;
722
+ }
723
+
724
+ // Table does not exist
725
+ $result = $this->productionDb->query( "SHOW TABLES LIKE '{$old}'" );
726
+ if( !$result || 0 === $result ) {
727
+ return false;
728
+ }
729
+
730
+ if( 0 != $this->options->job->start ) {
731
+ return true;
732
+ }
733
+
734
+ $this->options->job->total = ( int ) $this->productionDb->get_var( "SELECT COUNT(1) FROM {$old}" );
735
+
736
+ if( 0 == $this->options->job->total ) {
737
+ $this->finishStep();
738
+ return false;
739
+ }
740
+
741
+ return true;
742
+ }
743
+
744
+ /**
745
+ * Is table excluded from search replace processing?
746
+ * @param string $table
747
+ * @return boolean
748
+ */
749
+ private function isExcludedTable( $table ) {
750
+
751
+ $customTables = apply_filters( 'wpstg_clone_searchreplace_tables_exclude', array() );
752
+ $defaultTables = array('blogs');
753
+
754
+ $tables = array_merge( $customTables, $defaultTables );
755
+
756
+ $excludedTables = array();
757
+ foreach ( $tables as $key => $value ) {
758
+ $excludedTables[] = $this->options->prefix . $value;
759
+ }
760
+
761
+ if( in_array( $table, $excludedTables ) ) {
762
+ return true;
763
+ }
764
+ return false;
765
+ }
766
+
767
+ /**
768
+ * Finish the step
769
+ */
770
+ private function finishStep() {
771
+ // This job is not finished yet
772
+ if( $this->options->job->total > $this->options->job->start ) {
773
+ return false;
774
+ }
775
+
776
+ // Add it to cloned tables listing
777
+ $this->options->clonedTables[] = $this->options->tables[$this->options->currentStep];
778
+
779
+ // Reset job
780
+ $this->options->job = new \stdClass();
781
+
782
+ return true;
783
+ }
784
+
785
+ /**
786
+ * Drop table if necessary
787
+ * @param string $new
788
+ */
789
+ private function dropTable( $new ) {
790
+ $old = $this->stagingDb->get_var( $this->stagingDb->prepare( "SHOW TABLES LIKE %s", $new ) );
791
+
792
+ if( !$this->shouldDropTable( $new, $old ) ) {
793
+ return;
794
+ }
795
+
796
+ $this->log( "DB Search & Replace: {$new} already exists, dropping it first" );
797
+ $this->stagingDb->query( "DROP TABLE {$new}" );
798
+ }
799
+
800
+ /**
801
+ * Check if table needs to be dropped
802
+ * @param string $new
803
+ * @param string $old
804
+ * @return bool
805
+ */
806
+ private function shouldDropTable( $new, $old ) {
807
+ return (
808
+ $old == $new &&
809
+ (
810
+ !isset( $this->options->job->current ) ||
811
+ !isset( $this->options->job->start ) ||
812
+ 0 == $this->options->job->start
813
+ )
814
+ );
815
+ }
816
+
817
+ /**
818
+ * Check if WP is installed in subdir
819
+ * @return boolean
820
+ */
821
+ private function isSubDir() {
822
+ // Compare names without scheme to bypass cases where siteurl and home have different schemes http / https
823
+ // This is happening much more often than you would expect
824
+ $siteurl = preg_replace( '#^https?://#', '', rtrim( get_option( 'siteurl' ), '/' ) );
825
+ $home = preg_replace( '#^https?://#', '', rtrim( get_option( 'home' ), '/' ) );
826
+
827
+ if( $home !== $siteurl ) {
828
+ return true;
829
+ }
830
+ return false;
831
+ }
832
+
833
+ }
apps/Core/WPStaging.php CHANGED
@@ -29,7 +29,7 @@ final class WPStaging {
29
  /**
30
  * Plugin version
31
  */
32
- const VERSION = "2.6.3";
33
 
34
  /**
35
  * Plugin name
@@ -44,7 +44,7 @@ final class WPStaging {
44
  /**
45
  * Compatible WP Version
46
  */
47
- const WP_COMPATIBLE = "5.2.4";
48
 
49
  /**
50
  * Slug: Either wp-staging or wp-staging-pro
29
  /**
30
  * Plugin version
31
  */
32
+ const VERSION = "2.6.4";
33
 
34
  /**
35
  * Plugin name
44
  /**
45
  * Compatible WP Version
46
  */
47
+ const WP_COMPATIBLE = "5.3.0";
48
 
49
  /**
50
  * Slug: Either wp-staging or wp-staging-pro
readme.txt CHANGED
@@ -8,11 +8,11 @@ License: GPLv2 or later
8
  License URI: http://www.gnu.org/licenses/gpl-2.0.html
9
  Tags: staging, duplication, cloning, clone, migration, sandbox, test site, testing, backup, post, admin, administration, duplicate posts
10
  Requires at least: 3.6+
11
- Tested up to: 5.2
12
- Stable tag: 2.6.3
13
  Requires PHP: 5.3
14
 
15
- A duplicator plugin! Clone, duplicate and migrate live sites to independent staging and development sites that are available only to administrators.
16
 
17
  == Description ==
18
 
@@ -153,6 +153,9 @@ https://wp-staging.com
153
 
154
  == Changelog ==
155
 
 
 
 
156
  = 2.6.3 =
157
  * New Support for WordPress 5.2.4
158
  * New: Allow wildcards for excluding files
8
  License URI: http://www.gnu.org/licenses/gpl-2.0.html
9
  Tags: staging, duplication, cloning, clone, migration, sandbox, test site, testing, backup, post, admin, administration, duplicate posts
10
  Requires at least: 3.6+
11
+ Tested up to: 5.3
12
+ Stable tag: 2.6.4
13
  Requires PHP: 5.3
14
 
15
+ A duplicator plugin - clone/move, duplicate & migrate live websites to independent staging and development sites that are accessible​ by authorized users only.
16
 
17
  == Description ==
18
 
153
 
154
  == Changelog ==
155
 
156
+ = 2.6.4 =
157
+ * Fix: Broken image folder with duplicate leading slash after cloning
158
+
159
  = 2.6.3 =
160
  * New Support for WordPress 5.2.4
161
  * New: Allow wildcards for excluding files
wp-staging.php CHANGED
@@ -7,7 +7,7 @@
7
  * Author: WP-Staging
8
  * Author URI: https://wp-staging.com
9
  * Contributors: ReneHermi, ilgityildirim
10
- * Version: 2.6.3
11
  * Text Domain: wp-staging
12
  * Domain Path: /languages/
13
 
@@ -51,7 +51,7 @@ if( !defined( 'WPSTG_PLUGIN_URL' ) ) {
51
 
52
  // Version
53
  if( !defined( 'WPSTG_VERSION' ) ) {
54
- define( 'WPSTG_VERSION', '2.6.3' );
55
  }
56
 
57
  // Must use version of the optimizer
7
  * Author: WP-Staging
8
  * Author URI: https://wp-staging.com
9
  * Contributors: ReneHermi, ilgityildirim
10
+ * Version: 2.6.4
11
  * Text Domain: wp-staging
12
  * Domain Path: /languages/
13
 
51
 
52
  // Version
53
  if( !defined( 'WPSTG_VERSION' ) ) {
54
+ define( 'WPSTG_VERSION', '2.6.4' );
55
  }
56
 
57
  // Must use version of the optimizer