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

Version Description

  • Fix: Error notice if user tries to update staging site located in another database
  • Fix: Search & Replace adds staging site subfolder multiple times to replaced string when staging site is updated
  • Fix: Missing ajax delay setting
Download this release

Release Info

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

Code changes from version 2.4.0 to 2.4.1

apps/Backend/Modules/Jobs/Database.php CHANGED
@@ -1,9 +1,9 @@
1
  <?php
 
2
  namespace WPStaging\Backend\Modules\Jobs;
3
 
4
  // No Direct Access
5
- if (!defined("WPINC"))
6
- {
7
  die;
8
  }
9
 
@@ -14,8 +14,7 @@ use WPStaging\Utils\Strings;
14
  * Class Database
15
  * @package WPStaging\Backend\Modules\Jobs
16
  */
17
- class Database extends JobExecutable
18
- {
19
 
20
  /**
21
  * @var int
@@ -30,36 +29,45 @@ class Database extends JobExecutable
30
  /**
31
  * Initialize
32
  */
33
- public function initialize()
34
- {
35
  // Variables
36
- $this->total = count($this->options->tables);
37
- $this->db = WPStaging::getInstance()->get("wpdb");
 
38
  $this->isFatalError();
 
 
 
 
 
 
 
 
 
39
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  }
41
 
42
-
43
  /**
44
- * Return fatal error and stops here if subfolder already exists
45
- * and mainJob is not updating the clone
46
- * @return boolean
47
- */
48
- private function isFatalError(){
49
- $path = trailingslashit(get_home_path()) . $this->options->cloneDirectoryName;
50
- if (isset($this->options->mainJob) && $this->options->mainJob !== 'updating' && is_dir($path)){
51
- $this->returnException( " Can not continue! Change the name of the clone or delete existing folder. Then try again. Folder already exists: " . $path );
52
- }
53
- return false;
54
- }
55
-
56
- /**
57
  * Calculate Total Steps in This Job and Assign It to $this->options->totalSteps
58
  * @return void
59
  */
60
- protected function calculateTotalSteps()
61
- {
62
- $this->options->totalSteps = $this->total === 0 ? 1 : $this->total;
63
  }
64
 
65
  /**
@@ -67,21 +75,18 @@ class Database extends JobExecutable
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
- {
72
  // Over limits threshold
73
- if ($this->isOverThreshold())
74
- {
75
  // Prepare response and save current progress
76
- $this->prepareResponse(false, false);
77
  $this->saveOptions();
78
  return false;
79
  }
80
 
81
  // No more steps, finished
82
- if ($this->options->currentStep > $this->total || !isset($this->options->tables[$this->options->currentStep]))
83
- {
84
- $this->prepareResponse(true, false);
85
  return false;
86
  }
87
 
@@ -91,13 +96,11 @@ class Database extends JobExecutable
91
  // $this->prepareResponse();
92
  // return true;
93
  // }
94
-
95
  // Copy table
96
  //if (!$this->copyTable($this->options->tables[$this->options->currentStep]->name))
97
- if (!$this->copyTable($this->options->tables[$this->options->currentStep]))
98
- {
99
  // Prepare Response
100
- $this->prepareResponse(false, false);
101
 
102
  // Not finished
103
  return true;
@@ -114,12 +117,13 @@ class Database extends JobExecutable
114
  * Get new prefix for the staging site
115
  * @return string
116
  */
117
- private function getStagingPrefix(){
118
  $stagingPrefix = $this->options->prefix;
119
  // Make sure prefix of staging site is NEVER identical to prefix of live site!
120
- if ( $stagingPrefix == $this->db->prefix ){
121
- wp_die('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 to support@wp-staging.com');
122
- }
 
123
  return $stagingPrefix;
124
  }
125
 
@@ -128,27 +132,25 @@ class Database extends JobExecutable
128
  * @param string $tableName
129
  * @return bool
130
  */
131
- private function copyTable($tableName)
132
- {
133
 
134
- $strings = new Strings();
135
- $tableName = is_object($tableName) ? $tableName->name : $tableName;
136
- $newTableName = $this->getStagingPrefix() . $strings->str_replace_first($this->db->prefix, null, $tableName);
137
 
138
  // Drop table if necessary
139
- $this->dropTable($newTableName);
140
 
141
  // Save current job
142
- $this->setJob($newTableName);
143
 
144
  // Beginning of the job
145
- if (!$this->startJob($newTableName, $tableName))
146
- {
147
  return true;
148
  }
149
 
150
  // Copy data
151
- $this->copyData($newTableName, $tableName);
152
 
153
  // Finis the step
154
  return $this->finishStep();
@@ -159,22 +161,20 @@ class Database extends JobExecutable
159
  * @param string $new
160
  * @param string $old
161
  */
162
- private function copyData($new, $old)
163
- {
164
- $rows = $this->options->job->start+$this->settings->queryLimit;
165
  $this->log(
166
- "DB Copy: {$old} as {$new} from {$this->options->job->start} to {$rows} records"
167
  );
168
 
169
  $limitation = '';
170
 
171
- if (0 < (int) $this->settings->queryLimit)
172
- {
173
  $limitation = " LIMIT {$this->settings->queryLimit} OFFSET {$this->options->job->start}";
174
  }
175
 
176
  $this->db->query(
177
- "INSERT INTO {$new} SELECT * FROM {$old} {$limitation}"
178
  );
179
 
180
  // Set new offset
@@ -185,10 +185,8 @@ class Database extends JobExecutable
185
  * Set the job
186
  * @param string $table
187
  */
188
- private function setJob($table)
189
- {
190
- if (isset($this->options->job->current))
191
- {
192
  return;
193
  }
194
 
@@ -202,21 +200,18 @@ class Database extends JobExecutable
202
  * @param string $old
203
  * @return bool
204
  */
205
- private function startJob($new, $old)
206
- {
207
- if (0 != $this->options->job->start)
208
- {
209
  return true;
210
  }
211
 
212
- $this->log("DB Copy: Creating table {$new}");
213
 
214
- $this->db->query("CREATE TABLE {$new} LIKE {$old}");
215
 
216
- $this->options->job->total = (int) $this->db->get_var("SELECT COUNT(1) FROM {$old}");
217
 
218
- if (0 == $this->options->job->total)
219
- {
220
  $this->finishStep();
221
  return false;
222
  }
@@ -227,19 +222,17 @@ class Database extends JobExecutable
227
  /**
228
  * Finish the step
229
  */
230
- private function finishStep()
231
- {
232
  // This job is not finished yet
233
- if ($this->options->job->total > $this->options->job->start)
234
- {
235
  return false;
236
  }
237
 
238
  // Add it to cloned tables listing
239
- $this->options->clonedTables[] = $this->options->tables[$this->options->currentStep];
240
 
241
  // Reset job
242
- $this->options->job = new \stdClass();
243
 
244
  return true;
245
  }
@@ -248,17 +241,15 @@ class Database extends JobExecutable
248
  * Drop table if necessary
249
  * @param string $new
250
  */
251
- private function dropTable($new)
252
- {
253
- $old = $this->db->get_var($this->db->prepare("SHOW TABLES LIKE %s", $new));
254
 
255
- if (!$this->shouldDropTable($new, $old))
256
- {
257
  return;
258
  }
259
 
260
- $this->log("DB Copy: {$new} already exists, dropping it first");
261
- $this->db->query("DROP TABLE {$new}");
262
  }
263
 
264
  /**
@@ -267,16 +258,15 @@ class Database extends JobExecutable
267
  * @param string $old
268
  * @return bool
269
  */
270
- private function shouldDropTable($new, $old)
271
- {
272
  return (
273
- $old === $new &&
274
- (
275
- !isset($this->options->job->current) ||
276
- !isset($this->options->job->start) ||
277
  0 == $this->options->job->start
278
- )
279
- );
280
  }
281
 
282
- }
1
  <?php
2
+
3
  namespace WPStaging\Backend\Modules\Jobs;
4
 
5
  // No Direct Access
6
+ if( !defined( "WPINC" ) ) {
 
7
  die;
8
  }
9
 
14
  * Class Database
15
  * @package WPStaging\Backend\Modules\Jobs
16
  */
17
+ class Database extends JobExecutable {
 
18
 
19
  /**
20
  * @var int
29
  /**
30
  * Initialize
31
  */
32
+ public function initialize() {
 
33
  // Variables
34
+ $this->isExternalDatabase();
35
+ $this->total = count( $this->options->tables );
36
+ $this->db = WPStaging::getInstance()->get( "wpdb" );
37
  $this->isFatalError();
38
+ }
39
+
40
+ /**
41
+ * Check if external database is used
42
+ * @return boolean
43
+ */
44
+ private function isExternalDatabase() {
45
+ if( !empty( $this->options->databaseUser ) ) {
46
+ $this->returnException( __("This staging site is located in another database and needs to be edited with <a href='https://wp-staging.com' target='_blank'>WP Staging Pro</a>","wp-staging") );
47
 
48
+ }
49
+ return false;
50
+ }
51
+
52
+ /**
53
+ * Return fatal error and stops here if subfolder already exists
54
+ * and mainJob is not updating the clone
55
+ * @return boolean
56
+ */
57
+ private function isFatalError() {
58
+ $path = trailingslashit( get_home_path() ) . $this->options->cloneDirectoryName;
59
+ if( isset( $this->options->mainJob ) && $this->options->mainJob !== 'updating' && is_dir( $path ) ) {
60
+ $this->returnException( " Can not continue! Change the name of the clone or delete existing folder. Then try again. Folder already exists: " . $path );
61
+ }
62
+ return false;
63
  }
64
 
 
65
  /**
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  * Calculate Total Steps in This Job and Assign It to $this->options->totalSteps
67
  * @return void
68
  */
69
+ protected function calculateTotalSteps() {
70
+ $this->options->totalSteps = $this->total === 0 ? 1 : $this->total;
 
71
  }
72
 
73
  /**
75
  * Returns false when over threshold limits are hit or when the job is done, true otherwise
76
  * @return bool
77
  */
78
+ protected function execute() {
 
79
  // Over limits threshold
80
+ if( $this->isOverThreshold() ) {
 
81
  // Prepare response and save current progress
82
+ $this->prepareResponse( false, false );
83
  $this->saveOptions();
84
  return false;
85
  }
86
 
87
  // No more steps, finished
88
+ if( $this->options->currentStep > $this->total || !isset( $this->options->tables[$this->options->currentStep] ) ) {
89
+ $this->prepareResponse( true, false );
 
90
  return false;
91
  }
92
 
96
  // $this->prepareResponse();
97
  // return true;
98
  // }
 
99
  // Copy table
100
  //if (!$this->copyTable($this->options->tables[$this->options->currentStep]->name))
101
+ if( !$this->copyTable( $this->options->tables[$this->options->currentStep] ) ) {
 
102
  // Prepare Response
103
+ $this->prepareResponse( false, false );
104
 
105
  // Not finished
106
  return true;
117
  * Get new prefix for the staging site
118
  * @return string
119
  */
120
+ private function getStagingPrefix() {
121
  $stagingPrefix = $this->options->prefix;
122
  // Make sure prefix of staging site is NEVER identical to prefix of live site!
123
+ if( $stagingPrefix == $this->db->prefix ) {
124
+ //wp_die('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 to support@wp-staging.com');
125
+ $this->returnException( '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 to support@wp-staging.com' );
126
+ }
127
  return $stagingPrefix;
128
  }
129
 
132
  * @param string $tableName
133
  * @return bool
134
  */
135
+ private function copyTable( $tableName ) {
 
136
 
137
+ $strings = new Strings();
138
+ $tableName = is_object( $tableName ) ? $tableName->name : $tableName;
139
+ $newTableName = $this->getStagingPrefix() . $strings->str_replace_first( $this->db->prefix, null, $tableName );
140
 
141
  // Drop table if necessary
142
+ $this->dropTable( $newTableName );
143
 
144
  // Save current job
145
+ $this->setJob( $newTableName );
146
 
147
  // Beginning of the job
148
+ if( !$this->startJob( $newTableName, $tableName ) ) {
 
149
  return true;
150
  }
151
 
152
  // Copy data
153
+ $this->copyData( $newTableName, $tableName );
154
 
155
  // Finis the step
156
  return $this->finishStep();
161
  * @param string $new
162
  * @param string $old
163
  */
164
+ private function copyData( $new, $old ) {
165
+ $rows = $this->options->job->start + $this->settings->queryLimit;
 
166
  $this->log(
167
+ "DB Copy: {$old} as {$new} from {$this->options->job->start} to {$rows} records"
168
  );
169
 
170
  $limitation = '';
171
 
172
+ if( 0 < ( int ) $this->settings->queryLimit ) {
 
173
  $limitation = " LIMIT {$this->settings->queryLimit} OFFSET {$this->options->job->start}";
174
  }
175
 
176
  $this->db->query(
177
+ "INSERT INTO {$new} SELECT * FROM {$old} {$limitation}"
178
  );
179
 
180
  // Set new offset
185
  * Set the job
186
  * @param string $table
187
  */
188
+ private function setJob( $table ) {
189
+ if( isset( $this->options->job->current ) ) {
 
 
190
  return;
191
  }
192
 
200
  * @param string $old
201
  * @return bool
202
  */
203
+ private function startJob( $new, $old ) {
204
+ if( 0 != $this->options->job->start ) {
 
 
205
  return true;
206
  }
207
 
208
+ $this->log( "DB Copy: Creating table {$new}" );
209
 
210
+ $this->db->query( "CREATE TABLE {$new} LIKE {$old}" );
211
 
212
+ $this->options->job->total = ( int ) $this->db->get_var( "SELECT COUNT(1) FROM {$old}" );
213
 
214
+ if( 0 == $this->options->job->total ) {
 
215
  $this->finishStep();
216
  return false;
217
  }
222
  /**
223
  * Finish the step
224
  */
225
+ private function finishStep() {
 
226
  // This job is not finished yet
227
+ if( $this->options->job->total > $this->options->job->start ) {
 
228
  return false;
229
  }
230
 
231
  // Add it to cloned tables listing
232
+ $this->options->clonedTables[] = $this->options->tables[$this->options->currentStep];
233
 
234
  // Reset job
235
+ $this->options->job = new \stdClass();
236
 
237
  return true;
238
  }
241
  * Drop table if necessary
242
  * @param string $new
243
  */
244
+ private function dropTable( $new ) {
245
+ $old = $this->db->get_var( $this->db->prepare( "SHOW TABLES LIKE %s", $new ) );
 
246
 
247
+ if( !$this->shouldDropTable( $new, $old ) ) {
 
248
  return;
249
  }
250
 
251
+ $this->log( "DB Copy: {$new} already exists, dropping it first" );
252
+ $this->db->query( "DROP TABLE {$new}" );
253
  }
254
 
255
  /**
258
  * @param string $old
259
  * @return bool
260
  */
261
+ private function shouldDropTable( $new, $old ) {
 
262
  return (
263
+ $old === $new &&
264
+ (
265
+ !isset( $this->options->job->current ) ||
266
+ !isset( $this->options->job->start ) ||
267
  0 == $this->options->job->start
268
+ )
269
+ );
270
  }
271
 
272
+ }
apps/Backend/Modules/Jobs/Multisite/Data.php CHANGED
@@ -1,1009 +1,1009 @@
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 Data 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 = WPStaging::getInstance()->get( "wpdb" );
44
-
45
- $this->prefix = $this->options->prefix;
46
-
47
- $this->getTables();
48
-
49
- // Fix current step
50
- if( 0 == $this->options->currentStep ) {
51
- $this->options->currentStep = 0;
52
- }
53
- }
54
-
55
- /**
56
- * Get a list of tables to copy
57
- */
58
- private function getTables() {
59
- $strings = new Strings();
60
- $this->tables = array();
61
- foreach ( $this->options->tables as $table ) {
62
- $this->tables[] = $this->options->prefix . $strings->str_replace_first( $this->db->prefix, null, $table );
63
- }
64
- // Add extra global tables from main multisite (wpstgx_users and wpstgx_usermeta)
65
- $this->tables[] = $this->options->prefix . $strings->str_replace_first( $this->db->prefix, null, 'users' );
66
- $this->tables[] = $this->options->prefix . $strings->str_replace_first( $this->db->prefix, null, 'usermeta' );
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 = 18;
75
- }
76
-
77
- /**
78
- * Start Module
79
- * @return object
80
- */
81
- public function start() {
82
- // Execute steps
83
- $this->run();
84
-
85
- // Save option, progress
86
- $this->saveOptions();
87
-
88
- return ( object ) $this->response;
89
- }
90
-
91
- /**
92
- * Execute the Current Step
93
- * Returns false when over threshold limits are hit or when the job is done, true otherwise
94
- * @return bool
95
- */
96
- protected function execute() {
97
- // Fatal error. Let this happen never and break here immediately
98
- if( $this->isRoot() ) {
99
- return false;
100
- }
101
-
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->isFinished() ) {
112
- $this->prepareResponse( true, false );
113
- return false;
114
- }
115
-
116
- // Execute step
117
- $stepMethodName = "step" . $this->options->currentStep;
118
- if( !$this->{$stepMethodName}() ) {
119
- $this->prepareResponse( false, false );
120
- return false;
121
- }
122
-
123
- // Prepare Response
124
- $this->prepareResponse();
125
-
126
- // Not finished
127
- return true;
128
- }
129
-
130
- /**
131
- * Checks Whether There is Any Job to Execute or Not
132
- * @return bool
133
- */
134
- protected function isFinished() {
135
- return (
136
- $this->options->currentStep > $this->options->totalSteps ||
137
- !method_exists( $this, "step" . $this->options->currentStep )
138
- );
139
- }
140
-
141
- /**
142
- * Check if current operation is done on the root folder or on the live DB
143
- * @return boolean
144
- */
145
- protected function isRoot() {
146
-
147
- // Prefix is the same as the one of live site
148
- //$wpdb = WPStaging::getInstance()->get( "wpdb" );
149
- if( $this->db->prefix === $this->prefix ) {
150
- return true;
151
- }
152
-
153
- // CloneName is empty
154
- $name = ( array ) $this->options->cloneDirectoryName;
155
- if( empty( $name ) ) {
156
- return true;
157
- }
158
-
159
- // Live domain === Staging domain
160
- if( $this->multisiteHomeDomain . $this->options->cloneDirectoryName === $this->multisiteHomeDomain ) {
161
- return true;
162
- }
163
-
164
- return false;
165
- }
166
-
167
- /**
168
- * Check if table exists
169
- * @param string $table
170
- * @return boolean
171
- */
172
- protected function isTable( $table ) {
173
- if( $this->db->get_var( "SHOW TABLES LIKE '{$table}'" ) != $table ) {
174
- $this->log( "Table {$table} does not exists", Logger::TYPE_ERROR );
175
- return false;
176
- }
177
- return true;
178
- }
179
-
180
- /**
181
- * Return absolute destination path
182
- * @return string
183
- */
184
- // private function getAbsDestination() {
185
- // if( empty( $this->options->cloneDir ) ) {
186
- // return \WPStaging\WPStaging::getWPpath();
187
- // }
188
- // return trailingslashit( $this->options->cloneDir );
189
- // }
190
-
191
- /**
192
- * Copy wp-config.php if it is located outside of root one level up
193
- * @todo Needs some more testing before it will be released
194
- * @return boolean
195
- */
196
- protected function step0() {
197
- $this->log( "Preparing Data Step0: Check if wp-config.php is located in root path", Logger::TYPE_INFO );
198
-
199
- $dir = trailingslashit( dirname( ABSPATH ) );
200
-
201
- $source = $dir . 'wp-config.php';
202
-
203
- $destination = $this->options->destinationDir . 'wp-config.php';
204
-
205
-
206
- // Do not do anything
207
- if( (!is_file( $source ) && !is_link( $source )) || is_file( $destination ) ) {
208
- $this->log( "Preparing Data Step0: Skip it", Logger::TYPE_INFO );
209
- return true;
210
- }
211
-
212
- // Copy target of a symbolic link
213
- if( is_link( $source ) ) {
214
- $this->log( "Preparing Data Step0: Symbolic link found...", Logger::TYPE_INFO );
215
- if( !@copy( readlink( $source ), $destination ) ) {
216
- $errors = error_get_last();
217
- $this->log( "Preparing Data Step0: Failed to copy wp-config.php! Error: {$errors['message']} {$source} -> {$destination}", Logger::TYPE_ERROR );
218
- return true;
219
- }
220
- }
221
-
222
- // Copy file wp-config.php
223
- if( !@copy( $source, $destination ) ) {
224
- $errors = error_get_last();
225
- $this->log( "Preparing Data Step0: Failed to copy wp-config.php! Error: {$errors['message']} {$source} -> {$destination}", Logger::TYPE_ERROR );
226
- return true;
227
- }
228
- $this->log( "Preparing Data Step0: Successfull", Logger::TYPE_INFO );
229
- return true;
230
- }
231
-
232
- /**
233
- * Replace "siteurl" and "home"
234
- * @return bool
235
- */
236
- protected function step1() {
237
- $this->log( "Preparing Data Step1: Updating siteurl and homeurl in {$this->prefix}options {$this->db->last_error}", Logger::TYPE_INFO );
238
-
239
- // Skip - Table does not exist
240
- if( false === $this->isTable( $this->prefix . 'options' ) ) {
241
- return true;
242
- }
243
- // Skip - Table is not selected or updated
244
- if( !in_array( $this->prefix . 'options', $this->tables ) ) {
245
- $this->log( "Preparing Data Step1: Skipping" );
246
- return true;
247
- }
248
-
249
- // Installed in sub-directory
250
- // if( $this->isSubDir() ) {
251
- // $this->log( "Preparing Data Step1: Updating siteurl and homeurl to " . rtrim( $this->multisiteHomeDomain, "/" ) . '/' . $this->options->cloneDirectoryName );
252
- // // Replace URLs
253
- // $result = $this->db->query(
254
- // $this->db->prepare(
255
- // "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'siteurl' or option_name='home'", rtrim( $this->multisiteHomeDomain, "/" ) . '/' . $this->options->cloneDirectoryName
256
- // )
257
- // );
258
- // } else
259
- // {
260
- // $this->log( "Preparing Data Step1: Updating siteurl and homeurl to " . rtrim( $this->multisiteHomeDomain, "/" ) . '/' . $this->options->cloneDirectoryName );
261
- // // Replace URLs
262
- // $result = $this->db->query(
263
- // $this->db->prepare(
264
- // "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'siteurl' or option_name='home'", $this->multisiteHomeDomain . '/' . $this->options->cloneDirectoryName
265
- // )
266
- // );
267
- // }
268
-
269
- $this->log( "Preparing Data Step1: Updating siteurl and homeurl to " . $this->getStagingSiteUrl() );
270
- // Replace URLs
271
- $result = $this->db->query(
272
- $this->db->prepare(
273
- "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'siteurl' or option_name='home'", $this->getStagingSiteUrl()
274
- )
275
- );
276
-
277
-
278
-
279
- // All good
280
- if( $result ) {
281
- return true;
282
- }
283
-
284
- $this->log( "Preparing Data Step1: Failed to update siteurl and homeurl in {$this->prefix}options {$this->db->last_error}", Logger::TYPE_ERROR );
285
- return false;
286
- }
287
-
288
- /**
289
- * Update "wpstg_is_staging_site"
290
- * @return bool
291
- */
292
- protected function step2() {
293
-
294
- $this->log( "Preparing Data Step2: Updating row wpstg_is_staging_site in {$this->prefix}options {$this->db->last_error}" );
295
-
296
- // Skip - Table does not exist
297
- if( false === $this->isTable( $this->prefix . 'options' ) ) {
298
- return true;
299
- }
300
- // Skip - Table is not selected or updated
301
- if( !in_array( $this->prefix . 'options', $this->tables ) ) {
302
- $this->log( "Preparing Data Step2: Skipping" );
303
- return true;
304
- }
305
-
306
- $result = $this->db->query(
307
- $this->db->prepare(
308
- "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'wpstg_is_staging_site'", "true"
309
- )
310
- );
311
-
312
- // No errors but no option name such as wpstg_is_staging_site
313
- if( '' === $this->db->last_error && 0 == $result ) {
314
- $result = $this->db->query(
315
- $this->db->prepare(
316
- "INSERT INTO {$this->prefix}options (option_name,option_value) VALUES ('wpstg_is_staging_site',%s)", "true"
317
- )
318
- );
319
- }
320
-
321
- // All good
322
- if( $result ) {
323
- return true;
324
- }
325
-
326
- $this->log( "Preparing Data Step2: Failed to update wpstg_is_staging_site in {$this->prefix}options {$this->db->last_error}", Logger::TYPE_ERROR );
327
- return false;
328
- }
329
-
330
- /**
331
- * Update rewrite_rules
332
- * @return bool
333
- */
334
- protected function step3() {
335
-
336
- $this->log( "Preparing Data Step3: Updating rewrite_rules in {$this->prefix}options {$this->db->last_error}" );
337
-
338
- // Skip - Table does not exist
339
- if( false === $this->isTable( $this->prefix . 'options' ) ) {
340
- return true;
341
- }
342
-
343
- // Skip - Table is not selected or updated
344
- if( !in_array( $this->prefix . 'options', $this->tables ) ) {
345
- $this->log( "Preparing Data Step3: Skipping" );
346
- return true;
347
- }
348
-
349
- $result = $this->db->query(
350
- $this->db->prepare(
351
- "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'rewrite_rules'", ' '
352
- )
353
- );
354
-
355
- // All good
356
- if( $result ) {
357
- return true;
358
- }
359
-
360
- $this->log( "Preparing Data Step3: Failed to update rewrite_rules in {$this->prefix}options {$this->db->last_error}", Logger::TYPE_ERROR );
361
- return true;
362
- }
363
-
364
- /**
365
- * Update Table Prefix in wp_usermeta and wp_options
366
- * @return bool
367
- */
368
- protected function step4() {
369
- $this->log( "Preparing Data Step4: Updating db prefix in {$this->prefix}usermeta. {$this->db->last_error}" );
370
-
371
- // Skip - Table does not exist
372
- if( false === $this->isTable( $this->prefix . 'usermeta' ) ) {
373
- return true;
374
- }
375
-
376
- // Skip - Table is not selected or updated
377
- if( !in_array( $this->prefix . 'usermeta', $this->tables ) ) {
378
- $this->log( "Preparing Data Step4: Skipping" );
379
- return true;
380
- }
381
-
382
- $update = $this->db->query(
383
- $this->db->prepare(
384
- "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 . "_%"
385
- )
386
- );
387
-
388
- if( !$update ) {
389
- $this->log( "Preparing Data Step4: Failed to update {$this->prefix}usermeta meta_key database table prefixes; {$this->db->last_error}", Logger::TYPE_ERROR );
390
- $this->returnException( "Data Crunching Step 4: Failed to update {$this->prefix}usermeta meta_key database table prefixes; {$this->db->last_error}" );
391
- return false;
392
- }
393
- return true;
394
- }
395
-
396
- /**
397
- * Update $table_prefix in wp-config.php
398
- * @return bool
399
- */
400
- protected function step5() {
401
- $path = $this->options->destinationDir . "wp-config.php";
402
-
403
- $this->log( "Preparing Data Step5: Updating table_prefix in {$path} to " . $this->prefix );
404
- if( false === ($content = file_get_contents( $path )) ) {
405
- $this->log( "Preparing Data Step5: Failed to update table_prefix in {$path}. Can't read contents", Logger::TYPE_ERROR );
406
- return false;
407
- }
408
-
409
- // Replace table prefix
410
- $content = str_replace( '$table_prefix', '$table_prefix = \'' . $this->prefix . '\';//', $content );
411
-
412
- // Replace URLs
413
- $content = str_replace( $this->multisiteHomeDomain, $this->getStagingSiteUrl(), $content );
414
-
415
- if( false === @file_put_contents( $path, $content ) ) {
416
- $this->log( "Preparing Data Step5: Failed to update $table_prefix in {$path} to " . $this->prefix . ". Can't save contents", Logger::TYPE_ERROR );
417
- return false;
418
- }
419
-
420
- return true;
421
- }
422
-
423
- /**
424
- * Reset index.php to original file
425
- * This is needed if live site is located in subfolder
426
- * Check first if main wordpress is used in subfolder and index.php in parent directory
427
- * @see: https://codex.wordpress.org/Giving_WordPress_Its_Own_Directory
428
- * @return bool
429
- */
430
- protected function step6() {
431
-
432
- if( !$this->isSubDir() ) {
433
- $this->debugLog( "Preparing Data Step6: WP installation is not in a subdirectory! All good, skipping this step" );
434
- return true;
435
- }
436
-
437
- $path = $this->options->destinationDir . "index.php";
438
-
439
- if( false === ($content = file_get_contents( $path )) ) {
440
- $this->log( "Preparing Data Step6: Failed to reset {$path} for sub directory; can't read contents", Logger::TYPE_ERROR );
441
- return false;
442
- }
443
-
444
-
445
- if( !preg_match( "/(require(.*)wp-blog-header.php' \);)/", $content, $matches ) ) {
446
- $this->log(
447
- "Preparing Data Step6: Failed to reset index.php for sub directory; wp-blog-header.php is missing", Logger::TYPE_ERROR
448
- );
449
- return false;
450
- }
451
- $this->log( "Preparing Data: WP installation is in a subdirectory. Progressing..." );
452
-
453
- $pattern = "/require(.*) dirname(.*) __FILE__ (.*) \. '(.*)wp-blog-header.php'(.*);/";
454
-
455
- $replace = "require( dirname( __FILE__ ) . '/wp-blog-header.php' ); // " . $matches[0];
456
- $replace.= " // Changed by WP-Staging";
457
-
458
-
459
-
460
- if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
461
- $this->log( "Preparing Data: Failed to reset index.php for sub directory; replacement failed", Logger::TYPE_ERROR );
462
- return false;
463
- }
464
-
465
- if( false === @file_put_contents( $path, $content ) ) {
466
- $this->log( "Preparing Data: Failed to reset index.php for sub directory; can't save contents", Logger::TYPE_ERROR );
467
- return false;
468
- }
469
- $this->Log( "Preparing Data: Finished Step 6 successfully" );
470
- return true;
471
- }
472
-
473
- /**
474
- * Update wpstg_rmpermalinks_executed
475
- * @return bool
476
- */
477
- protected function step7() {
478
-
479
- $this->log( "Preparing Data Step7: Updating wpstg_rmpermalinks_executed in {$this->prefix}options {$this->db->last_error}" );
480
-
481
- // Skip - Table does not exist
482
- if( false === $this->isTable( $this->prefix . 'options' ) ) {
483
- return true;
484
- }
485
-
486
- // Skip - Table is not selected or updated
487
- if( !in_array( $this->prefix . 'options', $this->tables ) ) {
488
- $this->log( "Preparing Data Step7: Skipping" );
489
- return true;
490
- }
491
-
492
- $result = $this->db->query(
493
- $this->db->prepare(
494
- "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'wpstg_rmpermalinks_executed'", ' '
495
- )
496
- );
497
-
498
- // All good
499
- if( $result ) {
500
- $this->Log( "Preparing Data Step7: Finished Step 7 successfully" );
501
- return true;
502
- }
503
-
504
- $this->log( "Failed to update wpstg_rmpermalinks_executed in {$this->prefix}options {$this->db->last_error}", Logger::TYPE_WARNING );
505
- return true;
506
- }
507
-
508
- /**
509
- * Update permalink_structure
510
- * @return bool
511
- */
512
- protected function step8() {
513
-
514
- $this->log( "Preparing Data Step8: Updating permalink_structure in {$this->prefix}options {$this->db->last_error}" );
515
-
516
- // Skip - Table does not exist
517
- if( false === $this->isTable( $this->prefix . 'options' ) ) {
518
- return true;
519
- }
520
-
521
- // Skip - Table is not selected or updated
522
- if( !in_array( $this->prefix . 'options', $this->tables ) ) {
523
- $this->log( "Preparing Data Step8: Skipping" );
524
- return true;
525
- }
526
-
527
- $result = $this->db->query(
528
- $this->db->prepare(
529
- "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'permalink_structure'", ' '
530
- )
531
- );
532
-
533
- // All good
534
- if( $result ) {
535
- $this->Log( "Preparing Data Step8: Finished Step 8 successfully" );
536
- return true;
537
- }
538
-
539
- $this->log( "Failed to update permalink_structure in {$this->prefix}options {$this->db->last_error}", Logger::TYPE_ERROR );
540
- return true;
541
- }
542
-
543
- /**
544
- * Update blog_public option to not allow staging site to be indexed by search engines
545
- * @return bool
546
- */
547
- protected function step9() {
548
-
549
- $this->log( "Preparing Data Step9: Set staging site to noindex" );
550
-
551
- if( false === $this->isTable( $this->prefix . 'options' ) ) {
552
- return true;
553
- }
554
-
555
- // Skip - Table is not selected or updated
556
- if( !in_array( $this->prefix . 'options', $this->tables ) ) {
557
- $this->log( "Preparing Data Step9: Skipping" );
558
- return true;
559
- }
560
-
561
- $result = $this->db->query(
562
- $this->db->prepare(
563
- "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'blog_public'", '0'
564
- )
565
- );
566
-
567
- // All good
568
- if( $result ) {
569
- $this->Log( "Preparing Data Step9: Finished Step 9 successfully" );
570
- return true;
571
- }
572
-
573
- $this->log( "Can not update staging site to noindex. Possible already done!", Logger::TYPE_WARNING );
574
- return true;
575
- }
576
-
577
- /**
578
- * Update WP_HOME in wp-config.php
579
- * @return bool
580
- */
581
- protected function step10() {
582
- $path = $this->options->destinationDir . "wp-config.php";
583
-
584
- $this->log( "Preparing Data Step10: Updating WP_HOME in wp-config.php to " . $this->getStagingSiteUrl() );
585
-
586
- if( false === ($content = file_get_contents( $path )) ) {
587
- $this->log( "Preparing Data Step10: Failed to update WP_HOME in wp-config.php. Can't read wp-config.php", Logger::TYPE_ERROR );
588
- return false;
589
- }
590
-
591
-
592
- // Get WP_HOME from wp-config.php
593
- preg_match( "/define\s*\(\s*'WP_HOME'\s*,\s*(.*)\s*\);/", $content, $matches );
594
-
595
- if( !empty( $matches[1] ) ) {
596
- $matches[1];
597
-
598
- $pattern = "/define\s*\(\s*'WP_HOME'\s*,\s*(.*)\s*\);/";
599
-
600
- $replace = "define('WP_HOME','" . $this->getStagingSiteUrl() . "'); // " . $matches[1];
601
- $replace.= " // Changed by WP-Staging";
602
-
603
- if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
604
- $this->log( "Preparing Data: Failed to reset index.php for sub directory; replacement failed", Logger::TYPE_ERROR );
605
- return false;
606
- }
607
- } else {
608
- $this->log( "Preparing Data Step10: WP_HOME not defined in wp-config.php. Skipping this step." );
609
- }
610
-
611
- if( false === @file_put_contents( $path, $content ) ) {
612
- $this->log( "Preparing Data Step11: Failed to update WP_SITEURL. Can't save contents", Logger::TYPE_ERROR );
613
- return false;
614
- }
615
- $this->Log( "Preparing Data: Finished Step 11 successfully" );
616
- return true;
617
- }
618
-
619
- /**
620
- * Update WP_SITEURL in wp-config.php
621
- * @return bool
622
- */
623
- protected function step11() {
624
- $path = $this->options->destinationDir . "wp-config.php";
625
-
626
- $this->log( "Preparing Data Step11: Updating WP_SITEURL in wp-config.php to " . $this->getStagingSiteUrl() );
627
-
628
- if( false === ($content = file_get_contents( $path )) ) {
629
- $this->log( "Preparing Data Step11: Failed to update WP_SITEURL in wp-config.php. Can't read wp-config.php", Logger::TYPE_ERROR );
630
- return false;
631
- }
632
-
633
-
634
- // Get WP_SITEURL from wp-config.php
635
- preg_match( "/define\s*\(\s*'WP_SITEURL'\s*,\s*(.*)\s*\);/", $content, $matches );
636
-
637
- if( !empty( $matches[1] ) ) {
638
- $matches[1];
639
-
640
- $pattern = "/define\s*\(\s*'WP_SITEURL'\s*,\s*(.*)\s*\);/";
641
-
642
- $replace = "define('WP_SITEURL','" . $this->getStagingSiteUrl() . "'); // " . $matches[1];
643
- $replace.= " // Changed by WP-Staging";
644
-
645
- if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
646
- $this->log( "Preparing Data Step11: Failed to update WP_SITEURL", Logger::TYPE_ERROR );
647
- return false;
648
- }
649
- } else {
650
- $this->log( "Preparing Data Step11: WP_SITEURL not defined in wp-config.php. Skipping this step." );
651
- }
652
-
653
-
654
- if( false === @file_put_contents( $path, $content ) ) {
655
- $this->log( "Preparing Data Step11: Failed to update WP_SITEURL. Can't save contents", Logger::TYPE_ERROR );
656
- return false;
657
- }
658
- $this->Log( "Preparing Data: Finished Step 11 successfully" );
659
- return true;
660
- }
661
-
662
- /**
663
- * Update WP_ALLOW_MULTISITE constant in wp-config.php
664
- * @return bool
665
- */
666
- protected function step12() {
667
- $path = $this->options->destinationDir . "wp-config.php";
668
-
669
- $this->log( "Preparing Data Step12: Updating WP_ALLOW_MULTISITE in wp-config.php to false" );
670
-
671
- if( false === ($content = file_get_contents( $path )) ) {
672
- $this->log( "Preparing Data Step12: Failed to update WP_ALLOW_MULTISITE in wp-config.php. Can't read wp-config.php", Logger::TYPE_ERROR );
673
- return false;
674
- }
675
-
676
-
677
- // Get WP_SITEURL from wp-config.php
678
- preg_match( "/define\s*\(\s*'WP_ALLOW_MULTISITE'\s*,\s*(.*)\s*\);/", $content, $matches );
679
-
680
- if( !empty( $matches[1] ) ) {
681
- $matches[1];
682
-
683
- $pattern = "/define\s*\(\s*'WP_ALLOW_MULTISITE'\s*,\s*(.*)\s*\);/";
684
-
685
- $replace = "define('WP_ALLOW_MULTISITE',false); // " . $matches[1];
686
- $replace.= " // Changed by WP-Staging";
687
-
688
- if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
689
- $this->log( "Preparing Data Step12: Failed to update WP_ALLOW_MULTISITE", Logger::TYPE_ERROR );
690
- return false;
691
- }
692
- } else {
693
- $this->log( "Preparing Data Step12: WP_ALLOW_MULTISITE not defined in wp-config.php. Skipping this step." );
694
- }
695
-
696
-
697
- if( false === @file_put_contents( $path, $content ) ) {
698
- $this->log( "Preparing Data Step12: Failed to update WP_ALLOW_MULTISITE. Can't save contents", Logger::TYPE_ERROR );
699
- return false;
700
- }
701
- $this->Log( "Preparing Data: Finished Step 12 successfully" );
702
- return true;
703
- }
704
-
705
- /**
706
- * Update MULTISITE constant in wp-config.php
707
- * @return bool
708
- */
709
- protected function step13() {
710
- $path = $this->options->destinationDir . "wp-config.php";
711
-
712
- $this->log( "Preparing Data Step13: Updating MULTISITE in wp-config.php to false" );
713
-
714
- if( false === ($content = file_get_contents( $path )) ) {
715
- $this->log( "Preparing Data Step13: Failed to update MULTISITE in wp-config.php. Can't read wp-config.php", Logger::TYPE_ERROR );
716
- return false;
717
- }
718
-
719
-
720
- // Get WP_SITEURL from wp-config.php
721
- preg_match( "/define\s*\(\s*'MULTISITE'\s*,\s*(.*)\s*\);/", $content, $matches );
722
-
723
- if( !empty( $matches[1] ) ) {
724
- $matches[1];
725
-
726
- $pattern = "/define\s*\(\s*'MULTISITE'\s*,\s*(.*)\s*\);/";
727
-
728
- $replace = "define('MULTISITE',false); // " . $matches[1];
729
- $replace.= " // Changed by WP-Staging";
730
-
731
- if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
732
- $this->log( "Preparing Data Step13: Failed to update MULTISITE", Logger::TYPE_ERROR );
733
- return false;
734
- }
735
- } else {
736
- $this->log( "Preparing Data Step13: MULTISITE not defined in wp-config.php. Skipping this step." );
737
- }
738
-
739
-
740
- if( false === @file_put_contents( $path, $content ) ) {
741
- $this->log( "Preparing Data Step13: Failed to update MULTISITE. Can't save contents", Logger::TYPE_ERROR );
742
- return false;
743
- }
744
- $this->Log( "Preparing Data: Finished Step 13 successfully" );
745
- return true;
746
- }
747
-
748
- /**
749
- * Get active_sitewide_plugins from wp_sitemeta and active_plugins from subsite
750
- * Merge both arrays and copy them to the staging site into active_plugins
751
- */
752
- protected function step14() {
753
-
754
-
755
- $this->log( "Data Crunching Step 14: Updating active_plugins" );
756
-
757
- if( false === $this->isTable( $this->prefix . 'options' ) ) {
758
- $this->log( 'Data Crunching Step 14: Fatal Error ' . $this->prefix . 'options does not exist' );
759
- $this->returnException( 'Data Crunching Step 14: Fatal Error ' . $this->prefix . 'options does not exist' );
760
- return false;
761
- }
762
-
763
- // Skip - Table is not selected or updated
764
- if( !in_array( $this->prefix . 'options', $this->tables ) ) {
765
- $this->log( "Preparing Data Step14: Skipping" );
766
- return true;
767
- }
768
-
769
- // Get active_plugins value from sub site options table
770
- $active_plugins = $this->db->get_var( "SELECT option_value FROM {$this->db->prefix}options WHERE option_name = 'active_plugins' " );
771
-
772
- if( !$active_plugins ) {
773
- $this->log( "Data Crunching Step 14: Option active_plugins are empty " );
774
- $active_plugins = array();
775
- }
776
- // Get active_sitewide_plugins value from main multisite wp_sitemeta table
777
- $active_sitewide_plugins = $this->db->get_var( "SELECT meta_value FROM {$this->db->base_prefix}sitemeta WHERE meta_key = 'active_sitewide_plugins' " );
778
-
779
- if( !$active_sitewide_plugins ) {
780
- $this->log( "Data Crunching Step 14: Options {$this->db->base_prefix}active_sitewide_plugins is empty " );
781
- $active_sitewide_plugins = array();
782
- }
783
-
784
- $active_sitewide_plugins = unserialize( $active_sitewide_plugins );
785
- $active_plugins = unserialize( $active_plugins );
786
-
787
- $all_plugins = array_merge( $active_plugins, array_keys( $active_sitewide_plugins ) );
788
-
789
- sort( $all_plugins );
790
-
791
-
792
- // Update active_plugins
793
- $update = $this->db->query(
794
- "UPDATE {$this->prefix}options SET option_value = '" . serialize( $all_plugins ) . "' WHERE option_name = 'active_plugins'"
795
- );
796
-
797
- if( false === $update ) {
798
- $this->log( "Data Crunching Step 14: Can not update option active_plugins in {$this->prefix}options", Logger::TYPE_WARNING );
799
- return false;
800
- }
801
-
802
- $this->log( "Data Crunching Step 14: Successfull!" );
803
- return true;
804
- }
805
-
806
- /**
807
- * Update Table Prefix in wp_options
808
- * @return bool
809
- */
810
- protected function step15() {
811
- $this->log( "Preparing Data Step15: Updating db prefix in {$this->prefix}options." );
812
-
813
- // Skip - Table does not exist
814
- if( false === $this->isTable( $this->prefix . 'options' ) ) {
815
- return true;
816
- }
817
-
818
- // Skip - Table is not selected or updated
819
- if( !in_array( $this->prefix . 'options', $this->tables ) ) {
820
- $this->log( "Preparing Data Step4: Skipping" );
821
- return true;
822
- }
823
-
824
-
825
- $this->log( "Updating db option_names in {$this->prefix}options. " );
826
-
827
- // Filter the rows below. Do not update them!
828
- $filters = array(
829
- 'wp_mail_smtp',
830
- 'wp_mail_smtp_version',
831
- 'wp_mail_smtp_debug',
832
- );
833
-
834
- $filters = apply_filters( 'wpstg_data_excl_rows', $filters );
835
-
836
- $where = "";
837
- foreach ( $filters as $filter ) {
838
- $where .= " AND option_name <> '" . $filter . "'";
839
- }
840
-
841
- $updateOptions = $this->db->query(
842
- $this->db->prepare(
843
- "UPDATE IGNORE {$this->prefix}options SET option_name= replace(option_name, %s, %s) WHERE option_name LIKE %s" . $where, $this->db->prefix, $this->prefix, $this->db->prefix . "_%"
844
- )
845
- );
846
-
847
- if( !$updateOptions ) {
848
- $this->log( "Preparing Data Step15: Failed to update db option_names in {$this->prefix}options. Error: {$this->db->last_error}", Logger::TYPE_WARNING );
849
- //$this->returnException( "Data Crunching Step 15: Failed to update db option_names in {$this->prefix}options. Error: {$this->db->last_error}" );
850
- return true;
851
- }
852
-
853
-
854
- return true;
855
- }
856
-
857
- /**
858
- * Change upload_path in wp_options (if it is defined)
859
- * @return bool
860
- */
861
- protected function step16() {
862
- $this->log( "Preparing Data Step16: Updating upload_path {$this->prefix}options." );
863
-
864
- // Skip - Table does not exist
865
- if( false === $this->isTable( $this->prefix . 'options' ) ) {
866
- return true;
867
- }
868
-
869
- $newUploadPath = $this->getNewUploadPath();
870
-
871
- if( false === $newUploadPath ) {
872
- $this->log( "Preparing Data Step16: Skipping" );
873
- return true;
874
- }
875
-
876
- // Skip - Table is not selected or updated
877
- if( !in_array( $this->prefix . 'options', $this->tables ) ) {
878
- $this->log( "Preparing Data Step16: Skipping" );
879
- return true;
880
- }
881
-
882
- $error = isset( $this->db->last_error ) ? 'Last error: ' . $this->db->last_error : '';
883
-
884
- $this->log( "Updating upload_path in {$this->prefix}options. {$error}" );
885
-
886
- $updateOptions = $this->db->query(
887
- $this->db->prepare(
888
- "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'upload_path'", $newUploadPath
889
- )
890
- );
891
-
892
- if( !$updateOptions ) {
893
- $this->log( "Preparing Data Step16: Failed to update upload_path in {$this->prefix}options. {$error}", Logger::TYPE_ERROR );
894
- return true;
895
- }
896
- $this->Log( "Preparing Data: Finished Step 16 successfully" );
897
- return true;
898
- }
899
-
900
- /**
901
- * Update WP_CACHE in wp-config.php
902
- * @return bool
903
- */
904
- protected function step17() {
905
- $path = $this->options->destinationDir . "wp-config.php";
906
-
907
- $this->log( "Preparing Data Step17: Set WP_CACHE in wp-config.php to false" );
908
-
909
- if( false === ($content = file_get_contents( $path )) ) {
910
- $this->log( "Preparing Data Step17: Failed to update WP_CACHE in wp-config.php. Can't read wp-config.php", Logger::TYPE_ERROR );
911
- return false;
912
- }
913
-
914
-
915
- // Get WP_CACHE from wp-config.php
916
- preg_match( "/define\s*\(\s*'WP_CACHE'\s*,\s*(.*)\s*\);/", $content, $matches );
917
-
918
- if( !empty( $matches[1] ) ) {
919
- $matches[1];
920
-
921
- $pattern = "/define\s*\(\s*'WP_CACHE'\s*,\s*(.*)\s*\);/";
922
-
923
- $replace = "define('WP_CACHE',false); // " . $matches[1];
924
- $replace.= " // Changed by WP-Staging";
925
-
926
- if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
927
- $this->log( "Preparing Data: Failed to change WP_CACHE", Logger::TYPE_ERROR );
928
- return false;
929
- }
930
- } else {
931
- $this->log( "Preparing Data Step17: WP_CACHE not defined in wp-config.php. Skipping this step." );
932
- }
933
-
934
- if( false === @file_put_contents( $path, $content ) ) {
935
- $this->log( "Preparing Data Step17: Failed to update WP_CACHE. Can't save contents", Logger::TYPE_ERROR );
936
- return false;
937
- }
938
- $this->Log( "Preparing Data: Finished Step 17 successfully" );
939
- return true;
940
- }
941
-
942
- /**
943
- * Get Upload Path to staging site
944
- * @return boolean|string
945
- */
946
- protected function getNewUploadPath() {
947
- $uploadPath = get_option( 'upload_path' );
948
-
949
- if( !$uploadPath ) {
950
- return false;
951
- }
952
-
953
- $customSlug = str_replace( \WPStaging\WPStaging::getWPpath(), '', $uploadPath );
954
-
955
- $newUploadPath = \WPStaging\WPStaging::getWPpath() . $this->options->cloneDirectoryName . DIRECTORY_SEPARATOR . $customSlug;
956
-
957
- return $newUploadPath;
958
- }
959
-
960
- /**
961
- * Return URL to staging site
962
- * @return string
963
- */
964
- protected function getStagingSiteUrl() {
965
-
966
- if( !empty( $this->options->cloneHostname ) ) {
967
- return $this->options->cloneHostname;
968
- }
969
-
970
- if( $this->isSubDir() ) {
971
- return trailingslashit( $this->multisiteHomeDomain ) . trailingslashit($this->getSubDir()) . $this->options->cloneDirectoryName;
972
- }
973
-
974
- return trailingslashit( $this->multisiteHomeDomain ) . $this->options->cloneDirectoryName;
975
- }
976
-
977
- /**
978
- * Check if WP is installed in subdir
979
- * @return boolean
980
- */
981
- protected function isSubDir() {
982
- // Compare names without scheme to bypass cases where siteurl and home have different schemes http / https
983
- // This is happening much more often than you would expect
984
- $siteurl = preg_replace( '#^https?://#', '', rtrim( get_option( 'siteurl' ), '/' ) );
985
- $home = preg_replace( '#^https?://#', '', rtrim( get_option( 'home' ), '/' ) );
986
-
987
- if( $home !== $siteurl ) {
988
- return true;
989
- }
990
- return false;
991
- }
992
-
993
- /**
994
- * Get the install sub directory if WP is installed in sub directory
995
- * @return string
996
- */
997
- protected function getSubDir() {
998
- $home = get_option( 'home' );
999
- $siteurl = get_option( 'siteurl' );
1000
-
1001
- if( empty( $home ) || empty( $siteurl ) ) {
1002
- return '';
1003
- }
1004
-
1005
- $dir = str_replace( $home, '', $siteurl );
1006
- return str_replace( '/', '', $dir );
1007
- }
1008
-
1009
- }
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 Data 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 = WPStaging::getInstance()->get( "wpdb" );
44
+
45
+ $this->prefix = $this->options->prefix;
46
+
47
+ $this->getTables();
48
+
49
+ // Fix current step
50
+ if( 0 == $this->options->currentStep ) {
51
+ $this->options->currentStep = 0;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Get a list of tables to copy
57
+ */
58
+ private function getTables() {
59
+ $strings = new Strings();
60
+ $this->tables = array();
61
+ foreach ( $this->options->tables as $table ) {
62
+ $this->tables[] = $this->options->prefix . $strings->str_replace_first( $this->db->prefix, null, $table );
63
+ }
64
+ // Add extra global tables from main multisite (wpstgx_users and wpstgx_usermeta)
65
+ $this->tables[] = $this->options->prefix . $strings->str_replace_first( $this->db->prefix, null, 'users' );
66
+ $this->tables[] = $this->options->prefix . $strings->str_replace_first( $this->db->prefix, null, 'usermeta' );
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 = 18;
75
+ }
76
+
77
+ /**
78
+ * Start Module
79
+ * @return object
80
+ */
81
+ public function start() {
82
+ // Execute steps
83
+ $this->run();
84
+
85
+ // Save option, progress
86
+ $this->saveOptions();
87
+
88
+ return ( object ) $this->response;
89
+ }
90
+
91
+ /**
92
+ * Execute the Current Step
93
+ * Returns false when over threshold limits are hit or when the job is done, true otherwise
94
+ * @return bool
95
+ */
96
+ protected function execute() {
97
+ // Fatal error. Let this happen never and break here immediately
98
+ if( $this->isRoot() ) {
99
+ return false;
100
+ }
101
+
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->isFinished() ) {
112
+ $this->prepareResponse( true, false );
113
+ return false;
114
+ }
115
+
116
+ // Execute step
117
+ $stepMethodName = "step" . $this->options->currentStep;
118
+ if( !$this->{$stepMethodName}() ) {
119
+ $this->prepareResponse( false, false );
120
+ return false;
121
+ }
122
+
123
+ // Prepare Response
124
+ $this->prepareResponse();
125
+
126
+ // Not finished
127
+ return true;
128
+ }
129
+
130
+ /**
131
+ * Checks Whether There is Any Job to Execute or Not
132
+ * @return bool
133
+ */
134
+ protected function isFinished() {
135
+ return (
136
+ $this->options->currentStep > $this->options->totalSteps ||
137
+ !method_exists( $this, "step" . $this->options->currentStep )
138
+ );
139
+ }
140
+
141
+ /**
142
+ * Check if current operation is done on the root folder or on the live DB
143
+ * @return boolean
144
+ */
145
+ protected function isRoot() {
146
+
147
+ // Prefix is the same as the one of live site
148
+ //$wpdb = WPStaging::getInstance()->get( "wpdb" );
149
+ if( $this->db->prefix === $this->prefix ) {
150
+ return true;
151
+ }
152
+
153
+ // CloneName is empty
154
+ $name = ( array ) $this->options->cloneDirectoryName;
155
+ if( empty( $name ) ) {
156
+ return true;
157
+ }
158
+
159
+ // Live domain === Staging domain
160
+ if( $this->multisiteHomeDomain . $this->options->cloneDirectoryName === $this->multisiteHomeDomain ) {
161
+ return true;
162
+ }
163
+
164
+ return false;
165
+ }
166
+
167
+ /**
168
+ * Check if table exists
169
+ * @param string $table
170
+ * @return boolean
171
+ */
172
+ protected function isTable( $table ) {
173
+ if( $this->db->get_var( "SHOW TABLES LIKE '{$table}'" ) != $table ) {
174
+ $this->log( "Table {$table} does not exists", Logger::TYPE_ERROR );
175
+ return false;
176
+ }
177
+ return true;
178
+ }
179
+
180
+ /**
181
+ * Return absolute destination path
182
+ * @return string
183
+ */
184
+ // private function getAbsDestination() {
185
+ // if( empty( $this->options->cloneDir ) ) {
186
+ // return \WPStaging\WPStaging::getWPpath();
187
+ // }
188
+ // return trailingslashit( $this->options->cloneDir );
189
+ // }
190
+
191
+ /**
192
+ * Copy wp-config.php if it is located outside of root one level up
193
+ * @todo Needs some more testing before it will be released
194
+ * @return boolean
195
+ */
196
+ protected function step0() {
197
+ $this->log( "Preparing Data Step0: Check if wp-config.php is located in root path", Logger::TYPE_INFO );
198
+
199
+ $dir = trailingslashit( dirname( ABSPATH ) );
200
+
201
+ $source = $dir . 'wp-config.php';
202
+
203
+ $destination = $this->options->destinationDir . 'wp-config.php';
204
+
205
+
206
+ // Do not do anything
207
+ if( (!is_file( $source ) && !is_link( $source )) || is_file( $destination ) ) {
208
+ $this->log( "Preparing Data Step0: Skip it", Logger::TYPE_INFO );
209
+ return true;
210
+ }
211
+
212
+ // Copy target of a symbolic link
213
+ if( is_link( $source ) ) {
214
+ $this->log( "Preparing Data Step0: Symbolic link found...", Logger::TYPE_INFO );
215
+ if( !@copy( readlink( $source ), $destination ) ) {
216
+ $errors = error_get_last();
217
+ $this->log( "Preparing Data Step0: Failed to copy wp-config.php! Error: {$errors['message']} {$source} -> {$destination}", Logger::TYPE_ERROR );
218
+ return true;
219
+ }
220
+ }
221
+
222
+ // Copy file wp-config.php
223
+ if( !@copy( $source, $destination ) ) {
224
+ $errors = error_get_last();
225
+ $this->log( "Preparing Data Step0: Failed to copy wp-config.php! Error: {$errors['message']} {$source} -> {$destination}", Logger::TYPE_ERROR );
226
+ return true;
227
+ }
228
+ $this->log( "Preparing Data Step0: Successfull", Logger::TYPE_INFO );
229
+ return true;
230
+ }
231
+
232
+ /**
233
+ * Replace "siteurl" and "home"
234
+ * @return bool
235
+ */
236
+ protected function step1() {
237
+ $this->log( "Preparing Data Step1: Updating siteurl and homeurl in {$this->prefix}options {$this->db->last_error}", Logger::TYPE_INFO );
238
+
239
+ // Skip - Table does not exist
240
+ if( false === $this->isTable( $this->prefix . 'options' ) ) {
241
+ return true;
242
+ }
243
+ // Skip - Table is not selected or updated
244
+ if( !in_array( $this->prefix . 'options', $this->tables ) ) {
245
+ $this->log( "Preparing Data Step1: Skipping" );
246
+ return true;
247
+ }
248
+
249
+ // Installed in sub-directory
250
+ // if( $this->isSubDir() ) {
251
+ // $this->log( "Preparing Data Step1: Updating siteurl and homeurl to " . rtrim( $this->multisiteHomeDomain, "/" ) . '/' . $this->options->cloneDirectoryName );
252
+ // // Replace URLs
253
+ // $result = $this->db->query(
254
+ // $this->db->prepare(
255
+ // "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'siteurl' or option_name='home'", rtrim( $this->multisiteHomeDomain, "/" ) . '/' . $this->options->cloneDirectoryName
256
+ // )
257
+ // );
258
+ // } else
259
+ // {
260
+ // $this->log( "Preparing Data Step1: Updating siteurl and homeurl to " . rtrim( $this->multisiteHomeDomain, "/" ) . '/' . $this->options->cloneDirectoryName );
261
+ // // Replace URLs
262
+ // $result = $this->db->query(
263
+ // $this->db->prepare(
264
+ // "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'siteurl' or option_name='home'", $this->multisiteHomeDomain . '/' . $this->options->cloneDirectoryName
265
+ // )
266
+ // );
267
+ // }
268
+
269
+ $this->log( "Preparing Data Step1: Updating siteurl and homeurl to " . $this->getStagingSiteUrl() );
270
+ // Replace URLs
271
+ $result = $this->db->query(
272
+ $this->db->prepare(
273
+ "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'siteurl' or option_name='home'", $this->getStagingSiteUrl()
274
+ )
275
+ );
276
+
277
+
278
+
279
+ // All good
280
+ if( $result ) {
281
+ return true;
282
+ }
283
+
284
+ $this->log( "Preparing Data Step1: Failed to update siteurl and homeurl in {$this->prefix}options {$this->db->last_error}", Logger::TYPE_ERROR );
285
+ return false;
286
+ }
287
+
288
+ /**
289
+ * Update "wpstg_is_staging_site"
290
+ * @return bool
291
+ */
292
+ protected function step2() {
293
+
294
+ $this->log( "Preparing Data Step2: Updating row wpstg_is_staging_site in {$this->prefix}options {$this->db->last_error}" );
295
+
296
+ // Skip - Table does not exist
297
+ if( false === $this->isTable( $this->prefix . 'options' ) ) {
298
+ return true;
299
+ }
300
+ // Skip - Table is not selected or updated
301
+ if( !in_array( $this->prefix . 'options', $this->tables ) ) {
302
+ $this->log( "Preparing Data Step2: Skipping" );
303
+ return true;
304
+ }
305
+
306
+ $result = $this->db->query(
307
+ $this->db->prepare(
308
+ "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'wpstg_is_staging_site'", "true"
309
+ )
310
+ );
311
+
312
+ // No errors but no option name such as wpstg_is_staging_site
313
+ if( '' === $this->db->last_error && 0 == $result ) {
314
+ $result = $this->db->query(
315
+ $this->db->prepare(
316
+ "INSERT INTO {$this->prefix}options (option_name,option_value) VALUES ('wpstg_is_staging_site',%s)", "true"
317
+ )
318
+ );
319
+ }
320
+
321
+ // All good
322
+ if( $result ) {
323
+ return true;
324
+ }
325
+
326
+ $this->log( "Preparing Data Step2: Failed to update wpstg_is_staging_site in {$this->prefix}options {$this->db->last_error}", Logger::TYPE_ERROR );
327
+ return false;
328
+ }
329
+
330
+ /**
331
+ * Update rewrite_rules
332
+ * @return bool
333
+ */
334
+ protected function step3() {
335
+
336
+ $this->log( "Preparing Data Step3: Updating rewrite_rules in {$this->prefix}options {$this->db->last_error}" );
337
+
338
+ // Skip - Table does not exist
339
+ if( false === $this->isTable( $this->prefix . 'options' ) ) {
340
+ return true;
341
+ }
342
+
343
+ // Skip - Table is not selected or updated
344
+ if( !in_array( $this->prefix . 'options', $this->tables ) ) {
345
+ $this->log( "Preparing Data Step3: Skipping" );
346
+ return true;
347
+ }
348
+
349
+ $result = $this->db->query(
350
+ $this->db->prepare(
351
+ "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'rewrite_rules'", ' '
352
+ )
353
+ );
354
+
355
+ // All good
356
+ if( $result ) {
357
+ return true;
358
+ }
359
+
360
+ $this->log( "Preparing Data Step3: Failed to update rewrite_rules in {$this->prefix}options {$this->db->last_error}", Logger::TYPE_ERROR );
361
+ return true;
362
+ }
363
+
364
+ /**
365
+ * Update Table Prefix in wp_usermeta and wp_options
366
+ * @return bool
367
+ */
368
+ protected function step4() {
369
+ $this->log( "Preparing Data Step4: Updating db prefix in {$this->prefix}usermeta. {$this->db->last_error}" );
370
+
371
+ // Skip - Table does not exist
372
+ if( false === $this->isTable( $this->prefix . 'usermeta' ) ) {
373
+ return true;
374
+ }
375
+
376
+ // Skip - Table is not selected or updated
377
+ if( !in_array( $this->prefix . 'usermeta', $this->tables ) ) {
378
+ $this->log( "Preparing Data Step4: Skipping" );
379
+ return true;
380
+ }
381
+
382
+ $update = $this->db->query(
383
+ $this->db->prepare(
384
+ "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 . "_%"
385
+ )
386
+ );
387
+
388
+ if( !$update ) {
389
+ $this->log( "Preparing Data Step4: Failed to update {$this->prefix}usermeta meta_key database table prefixes; {$this->db->last_error}", Logger::TYPE_ERROR );
390
+ $this->returnException( "Data Crunching Step 4: Failed to update {$this->prefix}usermeta meta_key database table prefixes; {$this->db->last_error}" );
391
+ return false;
392
+ }
393
+ return true;
394
+ }
395
+
396
+ /**
397
+ * Update $table_prefix in wp-config.php
398
+ * @return bool
399
+ */
400
+ protected function step5() {
401
+ $path = $this->options->destinationDir . "wp-config.php";
402
+
403
+ $this->log( "Preparing Data Step5: Updating table_prefix in {$path} to " . $this->prefix );
404
+ if( false === ($content = file_get_contents( $path )) ) {
405
+ $this->log( "Preparing Data Step5: Failed to update table_prefix in {$path}. Can't read contents", Logger::TYPE_ERROR );
406
+ return false;
407
+ }
408
+
409
+ // Replace table prefix
410
+ $content = str_replace( '$table_prefix', '$table_prefix = \'' . $this->prefix . '\';//', $content );
411
+
412
+ // Replace URLs
413
+ $content = str_replace( $this->multisiteHomeDomain, $this->getStagingSiteUrl(), $content );
414
+
415
+ if( false === @file_put_contents( $path, $content ) ) {
416
+ $this->log( "Preparing Data Step5: Failed to update $table_prefix in {$path} to " . $this->prefix . ". Can't save contents", Logger::TYPE_ERROR );
417
+ return false;
418
+ }
419
+
420
+ return true;
421
+ }
422
+
423
+ /**
424
+ * Reset index.php to original file
425
+ * This is needed if live site is located in subfolder
426
+ * Check first if main wordpress is used in subfolder and index.php in parent directory
427
+ * @see: https://codex.wordpress.org/Giving_WordPress_Its_Own_Directory
428
+ * @return bool
429
+ */
430
+ protected function step6() {
431
+
432
+ if( !$this->isSubDir() ) {
433
+ $this->debugLog( "Preparing Data Step6: WP installation is not in a subdirectory! All good, skipping this step" );
434
+ return true;
435
+ }
436
+
437
+ $path = $this->options->destinationDir . "index.php";
438
+
439
+ if( false === ($content = file_get_contents( $path )) ) {
440
+ $this->log( "Preparing Data Step6: Failed to reset {$path} for sub directory; can't read contents", Logger::TYPE_ERROR );
441
+ return false;
442
+ }
443
+
444
+
445
+ if( !preg_match( "/(require(.*)wp-blog-header.php' \);)/", $content, $matches ) ) {
446
+ $this->log(
447
+ "Preparing Data Step6: Failed to reset index.php for sub directory; wp-blog-header.php is missing", Logger::TYPE_ERROR
448
+ );
449
+ return false;
450
+ }
451
+ $this->log( "Preparing Data: WP installation is in a subdirectory. Progressing..." );
452
+
453
+ $pattern = "/require(.*) dirname(.*) __FILE__ (.*) \. '(.*)wp-blog-header.php'(.*);/";
454
+
455
+ $replace = "require( dirname( __FILE__ ) . '/wp-blog-header.php' ); // " . $matches[0];
456
+ $replace.= " // Changed by WP-Staging";
457
+
458
+
459
+
460
+ if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
461
+ $this->log( "Preparing Data: Failed to reset index.php for sub directory; replacement failed", Logger::TYPE_ERROR );
462
+ return false;
463
+ }
464
+
465
+ if( false === @file_put_contents( $path, $content ) ) {
466
+ $this->log( "Preparing Data: Failed to reset index.php for sub directory; can't save contents", Logger::TYPE_ERROR );
467
+ return false;
468
+ }
469
+ $this->Log( "Preparing Data: Finished Step 6 successfully" );
470
+ return true;
471
+ }
472
+
473
+ /**
474
+ * Update wpstg_rmpermalinks_executed
475
+ * @return bool
476
+ */
477
+ protected function step7() {
478
+
479
+ $this->log( "Preparing Data Step7: Updating wpstg_rmpermalinks_executed in {$this->prefix}options {$this->db->last_error}" );
480
+
481
+ // Skip - Table does not exist
482
+ if( false === $this->isTable( $this->prefix . 'options' ) ) {
483
+ return true;
484
+ }
485
+
486
+ // Skip - Table is not selected or updated
487
+ if( !in_array( $this->prefix . 'options', $this->tables ) ) {
488
+ $this->log( "Preparing Data Step7: Skipping" );
489
+ return true;
490
+ }
491
+
492
+ $result = $this->db->query(
493
+ $this->db->prepare(
494
+ "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'wpstg_rmpermalinks_executed'", ' '
495
+ )
496
+ );
497
+
498
+ // All good
499
+ if( $result ) {
500
+ $this->Log( "Preparing Data Step7: Finished Step 7 successfully" );
501
+ return true;
502
+ }
503
+
504
+ $this->log( "Failed to update wpstg_rmpermalinks_executed in {$this->prefix}options {$this->db->last_error}", Logger::TYPE_WARNING );
505
+ return true;
506
+ }
507
+
508
+ /**
509
+ * Update permalink_structure
510
+ * @return bool
511
+ */
512
+ protected function step8() {
513
+
514
+ $this->log( "Preparing Data Step8: Updating permalink_structure in {$this->prefix}options {$this->db->last_error}" );
515
+
516
+ // Skip - Table does not exist
517
+ if( false === $this->isTable( $this->prefix . 'options' ) ) {
518
+ return true;
519
+ }
520
+
521
+ // Skip - Table is not selected or updated
522
+ if( !in_array( $this->prefix . 'options', $this->tables ) ) {
523
+ $this->log( "Preparing Data Step8: Skipping" );
524
+ return true;
525
+ }
526
+
527
+ $result = $this->db->query(
528
+ $this->db->prepare(
529
+ "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'permalink_structure'", ' '
530
+ )
531
+ );
532
+
533
+ // All good
534
+ if( $result ) {
535
+ $this->Log( "Preparing Data Step8: Finished Step 8 successfully" );
536
+ return true;
537
+ }
538
+
539
+ $this->log( "Failed to update permalink_structure in {$this->prefix}options {$this->db->last_error}", Logger::TYPE_ERROR );
540
+ return true;
541
+ }
542
+
543
+ /**
544
+ * Update blog_public option to not allow staging site to be indexed by search engines
545
+ * @return bool
546
+ */
547
+ protected function step9() {
548
+
549
+ $this->log( "Preparing Data Step9: Set staging site to noindex" );
550
+
551
+ if( false === $this->isTable( $this->prefix . 'options' ) ) {
552
+ return true;
553
+ }
554
+
555
+ // Skip - Table is not selected or updated
556
+ if( !in_array( $this->prefix . 'options', $this->tables ) ) {
557
+ $this->log( "Preparing Data Step9: Skipping" );
558
+ return true;
559
+ }
560
+
561
+ $result = $this->db->query(
562
+ $this->db->prepare(
563
+ "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'blog_public'", '0'
564
+ )
565
+ );
566
+
567
+ // All good
568
+ if( $result ) {
569
+ $this->Log( "Preparing Data Step9: Finished Step 9 successfully" );
570
+ return true;
571
+ }
572
+
573
+ $this->log( "Can not update staging site to noindex. Possible already done!", Logger::TYPE_WARNING );
574
+ return true;
575
+ }
576
+
577
+ /**
578
+ * Update WP_HOME in wp-config.php
579
+ * @return bool
580
+ */
581
+ protected function step10() {
582
+ $path = $this->options->destinationDir . "wp-config.php";
583
+
584
+ $this->log( "Preparing Data Step10: Updating WP_HOME in wp-config.php to " . $this->getStagingSiteUrl() );
585
+
586
+ if( false === ($content = file_get_contents( $path )) ) {
587
+ $this->log( "Preparing Data Step10: Failed to update WP_HOME in wp-config.php. Can't read wp-config.php", Logger::TYPE_ERROR );
588
+ return false;
589
+ }
590
+
591
+
592
+ // Get WP_HOME from wp-config.php
593
+ preg_match( "/define\s*\(\s*'WP_HOME'\s*,\s*(.*)\s*\);/", $content, $matches );
594
+
595
+ if( !empty( $matches[1] ) ) {
596
+ $matches[1];
597
+
598
+ $pattern = "/define\s*\(\s*'WP_HOME'\s*,\s*(.*)\s*\);/";
599
+
600
+ $replace = "define('WP_HOME','" . $this->getStagingSiteUrl() . "'); // " . $matches[1];
601
+ $replace.= " // Changed by WP-Staging";
602
+
603
+ if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
604
+ $this->log( "Preparing Data: Failed to reset index.php for sub directory; replacement failed", Logger::TYPE_ERROR );
605
+ return false;
606
+ }
607
+ } else {
608
+ $this->log( "Preparing Data Step10: WP_HOME not defined in wp-config.php. Skipping this step." );
609
+ }
610
+
611
+ if( false === @file_put_contents( $path, $content ) ) {
612
+ $this->log( "Preparing Data Step11: Failed to update WP_SITEURL. Can't save contents", Logger::TYPE_ERROR );
613
+ return false;
614
+ }
615
+ $this->Log( "Preparing Data: Finished Step 11 successfully" );
616
+ return true;
617
+ }
618
+
619
+ /**
620
+ * Update WP_SITEURL in wp-config.php
621
+ * @return bool
622
+ */
623
+ protected function step11() {
624
+ $path = $this->options->destinationDir . "wp-config.php";
625
+
626
+ $this->log( "Preparing Data Step11: Updating WP_SITEURL in wp-config.php to " . $this->getStagingSiteUrl() );
627
+
628
+ if( false === ($content = file_get_contents( $path )) ) {
629
+ $this->log( "Preparing Data Step11: Failed to update WP_SITEURL in wp-config.php. Can't read wp-config.php", Logger::TYPE_ERROR );
630
+ return false;
631
+ }
632
+
633
+
634
+ // Get WP_SITEURL from wp-config.php
635
+ preg_match( "/define\s*\(\s*'WP_SITEURL'\s*,\s*(.*)\s*\);/", $content, $matches );
636
+
637
+ if( !empty( $matches[1] ) ) {
638
+ $matches[1];
639
+
640
+ $pattern = "/define\s*\(\s*'WP_SITEURL'\s*,\s*(.*)\s*\);/";
641
+
642
+ $replace = "define('WP_SITEURL','" . $this->getStagingSiteUrl() . "'); // " . $matches[1];
643
+ $replace.= " // Changed by WP-Staging";
644
+
645
+ if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
646
+ $this->log( "Preparing Data Step11: Failed to update WP_SITEURL", Logger::TYPE_ERROR );
647
+ return false;
648
+ }
649
+ } else {
650
+ $this->log( "Preparing Data Step11: WP_SITEURL not defined in wp-config.php. Skipping this step." );
651
+ }
652
+
653
+
654
+ if( false === @file_put_contents( $path, $content ) ) {
655
+ $this->log( "Preparing Data Step11: Failed to update WP_SITEURL. Can't save contents", Logger::TYPE_ERROR );
656
+ return false;
657
+ }
658
+ $this->Log( "Preparing Data: Finished Step 11 successfully" );
659
+ return true;
660
+ }
661
+
662
+ /**
663
+ * Update WP_ALLOW_MULTISITE constant in wp-config.php
664
+ * @return bool
665
+ */
666
+ protected function step12() {
667
+ $path = $this->options->destinationDir . "wp-config.php";
668
+
669
+ $this->log( "Preparing Data Step12: Updating WP_ALLOW_MULTISITE in wp-config.php to false" );
670
+
671
+ if( false === ($content = file_get_contents( $path )) ) {
672
+ $this->log( "Preparing Data Step12: Failed to update WP_ALLOW_MULTISITE in wp-config.php. Can't read wp-config.php", Logger::TYPE_ERROR );
673
+ return false;
674
+ }
675
+
676
+
677
+ // Get WP_SITEURL from wp-config.php
678
+ preg_match( "/define\s*\(\s*'WP_ALLOW_MULTISITE'\s*,\s*(.*)\s*\);/", $content, $matches );
679
+
680
+ if( !empty( $matches[1] ) ) {
681
+ $matches[1];
682
+
683
+ $pattern = "/define\s*\(\s*'WP_ALLOW_MULTISITE'\s*,\s*(.*)\s*\);/";
684
+
685
+ $replace = "define('WP_ALLOW_MULTISITE',false); // " . $matches[1];
686
+ $replace.= " // Changed by WP-Staging";
687
+
688
+ if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
689
+ $this->log( "Preparing Data Step12: Failed to update WP_ALLOW_MULTISITE", Logger::TYPE_ERROR );
690
+ return false;
691
+ }
692
+ } else {
693
+ $this->log( "Preparing Data Step12: WP_ALLOW_MULTISITE not defined in wp-config.php. Skipping this step." );
694
+ }
695
+
696
+
697
+ if( false === @file_put_contents( $path, $content ) ) {
698
+ $this->log( "Preparing Data Step12: Failed to update WP_ALLOW_MULTISITE. Can't save contents", Logger::TYPE_ERROR );
699
+ return false;
700
+ }
701
+ $this->Log( "Preparing Data: Finished Step 12 successfully" );
702
+ return true;
703
+ }
704
+
705
+ /**
706
+ * Update MULTISITE constant in wp-config.php
707
+ * @return bool
708
+ */
709
+ protected function step13() {
710
+ $path = $this->options->destinationDir . "wp-config.php";
711
+
712
+ $this->log( "Preparing Data Step13: Updating MULTISITE in wp-config.php to false" );
713
+
714
+ if( false === ($content = file_get_contents( $path )) ) {
715
+ $this->log( "Preparing Data Step13: Failed to update MULTISITE in wp-config.php. Can't read wp-config.php", Logger::TYPE_ERROR );
716
+ return false;
717
+ }
718
+
719
+
720
+ // Get WP_SITEURL from wp-config.php
721
+ preg_match( "/define\s*\(\s*'MULTISITE'\s*,\s*(.*)\s*\);/", $content, $matches );
722
+
723
+ if( !empty( $matches[1] ) ) {
724
+ $matches[1];
725
+
726
+ $pattern = "/define\s*\(\s*'MULTISITE'\s*,\s*(.*)\s*\);/";
727
+
728
+ $replace = "define('MULTISITE',false); // " . $matches[1];
729
+ $replace.= " // Changed by WP-Staging";
730
+
731
+ if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
732
+ $this->log( "Preparing Data Step13: Failed to update MULTISITE", Logger::TYPE_ERROR );
733
+ return false;
734
+ }
735
+ } else {
736
+ $this->log( "Preparing Data Step13: MULTISITE not defined in wp-config.php. Skipping this step." );
737
+ }
738
+
739
+
740
+ if( false === @file_put_contents( $path, $content ) ) {
741
+ $this->log( "Preparing Data Step13: Failed to update MULTISITE. Can't save contents", Logger::TYPE_ERROR );
742
+ return false;
743
+ }
744
+ $this->Log( "Preparing Data: Finished Step 13 successfully" );
745
+ return true;
746
+ }
747
+
748
+ /**
749
+ * Get active_sitewide_plugins from wp_sitemeta and active_plugins from subsite
750
+ * Merge both arrays and copy them to the staging site into active_plugins
751
+ */
752
+ protected function step14() {
753
+
754
+
755
+ $this->log( "Data Crunching Step 14: Updating active_plugins" );
756
+
757
+ if( false === $this->isTable( $this->prefix . 'options' ) ) {
758
+ $this->log( 'Data Crunching Step 14: Fatal Error ' . $this->prefix . 'options does not exist' );
759
+ $this->returnException( 'Data Crunching Step 14: Fatal Error ' . $this->prefix . 'options does not exist' );
760
+ return false;
761
+ }
762
+
763
+ // Skip - Table is not selected or updated
764
+ if( !in_array( $this->prefix . 'options', $this->tables ) ) {
765
+ $this->log( "Preparing Data Step14: Skipping" );
766
+ return true;
767
+ }
768
+
769
+ // Get active_plugins value from sub site options table
770
+ $active_plugins = $this->db->get_var( "SELECT option_value FROM {$this->db->prefix}options WHERE option_name = 'active_plugins' " );
771
+
772
+ if( !$active_plugins ) {
773
+ $this->log( "Data Crunching Step 14: Option active_plugins are empty " );
774
+ $active_plugins = array();
775
+ }
776
+ // Get active_sitewide_plugins value from main multisite wp_sitemeta table
777
+ $active_sitewide_plugins = $this->db->get_var( "SELECT meta_value FROM {$this->db->base_prefix}sitemeta WHERE meta_key = 'active_sitewide_plugins' " );
778
+
779
+ if( !$active_sitewide_plugins ) {
780
+ $this->log( "Data Crunching Step 14: Options {$this->db->base_prefix}active_sitewide_plugins is empty " );
781
+ $active_sitewide_plugins = array();
782
+ }
783
+
784
+ $active_sitewide_plugins = unserialize( $active_sitewide_plugins );
785
+ $active_plugins = unserialize( $active_plugins );
786
+
787
+ $all_plugins = array_merge( $active_plugins, array_keys( $active_sitewide_plugins ) );
788
+
789
+ sort( $all_plugins );
790
+
791
+
792
+ // Update active_plugins
793
+ $update = $this->db->query(
794
+ "UPDATE {$this->prefix}options SET option_value = '" . serialize( $all_plugins ) . "' WHERE option_name = 'active_plugins'"
795
+ );
796
+
797
+ if( false === $update ) {
798
+ $this->log( "Data Crunching Step 14: Can not update option active_plugins in {$this->prefix}options", Logger::TYPE_WARNING );
799
+ return false;
800
+ }
801
+
802
+ $this->log( "Data Crunching Step 14: Successfull!" );
803
+ return true;
804
+ }
805
+
806
+ /**
807
+ * Update Table Prefix in wp_options
808
+ * @return bool
809
+ */
810
+ protected function step15() {
811
+ $this->log( "Preparing Data Step15: Updating db prefix in {$this->prefix}options." );
812
+
813
+ // Skip - Table does not exist
814
+ if( false === $this->isTable( $this->prefix . 'options' ) ) {
815
+ return true;
816
+ }
817
+
818
+ // Skip - Table is not selected or updated
819
+ if( !in_array( $this->prefix . 'options', $this->tables ) ) {
820
+ $this->log( "Preparing Data Step4: Skipping" );
821
+ return true;
822
+ }
823
+
824
+
825
+ $this->log( "Updating db option_names in {$this->prefix}options. " );
826
+
827
+ // Filter the rows below. Do not update them!
828
+ $filters = array(
829
+ 'wp_mail_smtp',
830
+ 'wp_mail_smtp_version',
831
+ 'wp_mail_smtp_debug',
832
+ );
833
+
834
+ $filters = apply_filters( 'wpstg_data_excl_rows', $filters );
835
+
836
+ $where = "";
837
+ foreach ( $filters as $filter ) {
838
+ $where .= " AND option_name <> '" . $filter . "'";
839
+ }
840
+
841
+ $updateOptions = $this->db->query(
842
+ $this->db->prepare(
843
+ "UPDATE IGNORE {$this->prefix}options SET option_name= replace(option_name, %s, %s) WHERE option_name LIKE %s" . $where, $this->db->prefix, $this->prefix, $this->db->prefix . "_%"
844
+ )
845
+ );
846
+
847
+ if( !$updateOptions ) {
848
+ $this->log( "Preparing Data Step15: Failed to update db option_names in {$this->prefix}options. Error: {$this->db->last_error}", Logger::TYPE_WARNING );
849
+ //$this->returnException( "Data Crunching Step 15: Failed to update db option_names in {$this->prefix}options. Error: {$this->db->last_error}" );
850
+ return true;
851
+ }
852
+
853
+
854
+ return true;
855
+ }
856
+
857
+ /**
858
+ * Change upload_path in wp_options (if it is defined)
859
+ * @return bool
860
+ */
861
+ protected function step16() {
862
+ $this->log( "Preparing Data Step16: Updating upload_path {$this->prefix}options." );
863
+
864
+ // Skip - Table does not exist
865
+ if( false === $this->isTable( $this->prefix . 'options' ) ) {
866
+ return true;
867
+ }
868
+
869
+ $newUploadPath = $this->getNewUploadPath();
870
+
871
+ if( false === $newUploadPath ) {
872
+ $this->log( "Preparing Data Step16: Skipping" );
873
+ return true;
874
+ }
875
+
876
+ // Skip - Table is not selected or updated
877
+ if( !in_array( $this->prefix . 'options', $this->tables ) ) {
878
+ $this->log( "Preparing Data Step16: Skipping" );
879
+ return true;
880
+ }
881
+
882
+ $error = isset( $this->db->last_error ) ? 'Last error: ' . $this->db->last_error : '';
883
+
884
+ $this->log( "Updating upload_path in {$this->prefix}options. {$error}" );
885
+
886
+ $updateOptions = $this->db->query(
887
+ $this->db->prepare(
888
+ "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'upload_path'", $newUploadPath
889
+ )
890
+ );
891
+
892
+ if( !$updateOptions ) {
893
+ $this->log( "Preparing Data Step16: Failed to update upload_path in {$this->prefix}options. {$error}", Logger::TYPE_ERROR );
894
+ return true;
895
+ }
896
+ $this->Log( "Preparing Data: Finished Step 16 successfully" );
897
+ return true;
898
+ }
899
+
900
+ /**
901
+ * Update WP_CACHE in wp-config.php
902
+ * @return bool
903
+ */
904
+ protected function step17() {
905
+ $path = $this->options->destinationDir . "wp-config.php";
906
+
907
+ $this->log( "Preparing Data Step17: Set WP_CACHE in wp-config.php to false" );
908
+
909
+ if( false === ($content = file_get_contents( $path )) ) {
910
+ $this->log( "Preparing Data Step17: Failed to update WP_CACHE in wp-config.php. Can't read wp-config.php", Logger::TYPE_ERROR );
911
+ return false;
912
+ }
913
+
914
+
915
+ // Get WP_CACHE from wp-config.php
916
+ preg_match( "/define\s*\(\s*'WP_CACHE'\s*,\s*(.*)\s*\);/", $content, $matches );
917
+
918
+ if( !empty( $matches[1] ) ) {
919
+ $matches[1];
920
+
921
+ $pattern = "/define\s*\(\s*'WP_CACHE'\s*,\s*(.*)\s*\);/";
922
+
923
+ $replace = "define('WP_CACHE',false); // " . $matches[1];
924
+ $replace.= " // Changed by WP-Staging";
925
+
926
+ if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
927
+ $this->log( "Preparing Data: Failed to change WP_CACHE", Logger::TYPE_ERROR );
928
+ return false;
929
+ }
930
+ } else {
931
+ $this->log( "Preparing Data Step17: WP_CACHE not defined in wp-config.php. Skipping this step." );
932
+ }
933
+
934
+ if( false === @file_put_contents( $path, $content ) ) {
935
+ $this->log( "Preparing Data Step17: Failed to update WP_CACHE. Can't save contents", Logger::TYPE_ERROR );
936
+ return false;
937
+ }
938
+ $this->Log( "Preparing Data: Finished Step 17 successfully" );
939
+ return true;
940
+ }
941
+
942
+ /**
943
+ * Get Upload Path to staging site
944
+ * @return boolean|string
945
+ */
946
+ protected function getNewUploadPath() {
947
+ $uploadPath = get_option( 'upload_path' );
948
+
949
+ if( !$uploadPath ) {
950
+ return false;
951
+ }
952
+
953
+ $customSlug = str_replace( \WPStaging\WPStaging::getWPpath(), '', $uploadPath );
954
+
955
+ $newUploadPath = \WPStaging\WPStaging::getWPpath() . $this->options->cloneDirectoryName . DIRECTORY_SEPARATOR . $customSlug;
956
+
957
+ return $newUploadPath;
958
+ }
959
+
960
+ /**
961
+ * Return URL to staging site
962
+ * @return string
963
+ */
964
+ protected function getStagingSiteUrl() {
965
+
966
+ if( !empty( $this->options->cloneHostname ) ) {
967
+ return $this->options->cloneHostname;
968
+ }
969
+
970
+ if( $this->isSubDir() ) {
971
+ return trailingslashit( $this->multisiteHomeDomain ) . trailingslashit($this->getSubDir()) . $this->options->cloneDirectoryName;
972
+ }
973
+
974
+ return trailingslashit( $this->multisiteHomeDomain ) . $this->options->cloneDirectoryName;
975
+ }
976
+
977
+ /**
978
+ * Check if WP is installed in subdir
979
+ * @return boolean
980
+ */
981
+ protected function isSubDir() {
982
+ // Compare names without scheme to bypass cases where siteurl and home have different schemes http / https
983
+ // This is happening much more often than you would expect
984
+ $siteurl = preg_replace( '#^https?://#', '', rtrim( get_option( 'siteurl' ), '/' ) );
985
+ $home = preg_replace( '#^https?://#', '', rtrim( get_option( 'home' ), '/' ) );
986
+
987
+ if( $home !== $siteurl ) {
988
+ return true;
989
+ }
990
+ return false;
991
+ }
992
+
993
+ /**
994
+ * Get the install sub directory if WP is installed in sub directory
995
+ * @return string
996
+ */
997
+ protected function getSubDir() {
998
+ $home = get_option( 'home' );
999
+ $siteurl = get_option( 'siteurl' );
1000
+
1001
+ if( empty( $home ) || empty( $siteurl ) ) {
1002
+ return '';
1003
+ }
1004
+
1005
+ $dir = str_replace( $home, '', $siteurl );
1006
+ return str_replace( '/', '', $dir );
1007
+ }
1008
+
1009
+ }
apps/Backend/Modules/Jobs/Multisite/DataExternal.php CHANGED
@@ -1,1135 +1,1135 @@
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, $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 = 19;
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
- $this->options->currentStep > $this->options->totalSteps ||
145
- !method_exists( $this, "step" . $this->options->currentStep )
146
- );
147
- }
148
-
149
- /**
150
- * Check if current operation is done on the root folder or on the live DB
151
- * @return boolean
152
- */
153
- protected function isRoot() {
154
-
155
- // Prefix is the same as the one of live site
156
- // $wpdb = WPStaging::getInstance()->get( "wpdb" );
157
- // if( $wpdb->prefix === $this->prefix ) {
158
- // return true;
159
- // }
160
- // CloneName is empty
161
- $name = ( array ) $this->options->cloneDirectoryName;
162
- if( empty( $name ) ) {
163
- return true;
164
- }
165
-
166
- // Live domain === Staging domain
167
- if( $this->multisiteHomeDomain . $this->options->cloneDirectoryName === $this->multisiteHomeDomain ) {
168
- return true;
169
- }
170
-
171
- return false;
172
- }
173
-
174
- /**
175
- * Check if table exists
176
- * @param string $table
177
- * @return boolean
178
- */
179
- protected function isTable( $table ) {
180
- if( $this->db->get_var( "SHOW TABLES LIKE '{$table}'" ) != $table ) {
181
- $this->log( "Table {$table} does not exists", Logger::TYPE_ERROR );
182
- return false;
183
- }
184
- return true;
185
- }
186
-
187
- /**
188
- * Return absolute destination path
189
- * @return string
190
- */
191
- // private function getAbsDestination() {
192
- // if( empty( $this->options->cloneDir ) ) {
193
- // return \WPStaging\WPStaging::getWPpath();
194
- // }
195
- // return trailingslashit( $this->options->cloneDir );
196
- // }
197
-
198
- /**
199
- * Copy wp-config.php if it is located outside of root one level up
200
- * @todo Needs some more testing before it will be released
201
- * @return boolean
202
- */
203
- protected function step0() {
204
- $this->log( "Preparing Data Step0: Check if wp-config.php is located in root path", Logger::TYPE_INFO );
205
-
206
- $dir = trailingslashit( dirname( ABSPATH ) );
207
-
208
- $source = $dir . 'wp-config.php';
209
-
210
- $destination = $this->options->destinationDir . 'wp-config.php';
211
-
212
-
213
- // Do not do anything
214
- if( (!is_file( $source ) && !is_link( $source )) || is_file( $destination ) ) {
215
- $this->log( "Preparing Data Step0: Skip it", Logger::TYPE_INFO );
216
- return true;
217
- }
218
-
219
- // Copy target of a symbolic link
220
- if( is_link( $source ) ) {
221
- $this->log( "Preparing Data Step0: Symbolic link found...", Logger::TYPE_INFO );
222
- if( !@copy( readlink( $source ), $destination ) ) {
223
- $errors = error_get_last();
224
- $this->log( "Preparing Data Step0: Failed to copy wp-config.php! Error: {$errors['message']} {$source} -> {$destination}", Logger::TYPE_ERROR );
225
- return true;
226
- }
227
- }
228
-
229
- // Copy file wp-config.php
230
- if( !@copy( $source, $destination ) ) {
231
- $errors = error_get_last();
232
- $this->log( "Preparing Data Step0: Failed to copy wp-config.php! Error: {$errors['message']} {$source} -> {$destination}", Logger::TYPE_ERROR );
233
- return true;
234
- }
235
- $this->log( "Preparing Data Step0: Successfull", Logger::TYPE_INFO );
236
- return true;
237
- }
238
-
239
- /**
240
- * Replace "siteurl" and "home"
241
- * @return bool
242
- */
243
- protected function step1() {
244
- $this->log( "Preparing Data Step1: Updating siteurl and homeurl in {$this->prefix}options {$this->db->last_error}", Logger::TYPE_INFO );
245
-
246
- // Skip - Table does not exist
247
- if( false === $this->isTable( $this->prefix . 'options' ) ) {
248
- return true;
249
- }
250
- // Skip - Table is not selected or updated
251
- if( !in_array( $this->prefix . 'options', $this->tables ) ) {
252
- $this->log( "Preparing Data Step1: Skipping" );
253
- return true;
254
- }
255
-
256
- // Installed in sub-directory
257
- // if( $this->isSubDir() ) {
258
- // $this->log( "Preparing Data Step1: Updating siteurl and homeurl to " . rtrim( $this->multisiteHomeDomain, "/" ) . '/' . $this->options->cloneDirectoryName );
259
- // // Replace URLs
260
- // $result = $this->db->query(
261
- // $this->db->prepare(
262
- // "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'siteurl' or option_name='home'", rtrim( $this->multisiteHomeDomain, "/" ) . '/' . $this->options->cloneDirectoryName
263
- // )
264
- // );
265
- // } else
266
- // {
267
- // $this->log( "Preparing Data Step1: Updating siteurl and homeurl to " . rtrim( $this->multisiteHomeDomain, "/" ) . '/' . $this->options->cloneDirectoryName );
268
- // // Replace URLs
269
- // $result = $this->db->query(
270
- // $this->db->prepare(
271
- // "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'siteurl' or option_name='home'", $this->multisiteHomeDomain . '/' . $this->options->cloneDirectoryName
272
- // )
273
- // );
274
- // }
275
-
276
- $this->log( "Preparing Data Step1: Updating siteurl and homeurl to " . $this->getStagingSiteUrl() );
277
- // Replace URLs
278
- $result = $this->db->query(
279
- $this->db->prepare(
280
- "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'siteurl' or option_name='home'", $this->getStagingSiteUrl()
281
- )
282
- );
283
-
284
-
285
-
286
- // All good
287
- if( $result ) {
288
- return true;
289
- }
290
-
291
- $this->log( "Preparing Data Step1: Failed to update siteurl and homeurl in {$this->prefix}options {$this->db->last_error}", Logger::TYPE_ERROR );
292
- return false;
293
- }
294
-
295
- /**
296
- * Update "wpstg_is_staging_site"
297
- * @return bool
298
- */
299
- protected function step2() {
300
-
301
- $this->log( "Preparing Data Step2: Updating row wpstg_is_staging_site in {$this->prefix}options {$this->db->last_error}" );
302
-
303
- // Skip - Table does not exist
304
- if( false === $this->isTable( $this->prefix . 'options' ) ) {
305
- return true;
306
- }
307
- // Skip - Table is not selected or updated
308
- if( !in_array( $this->prefix . 'options', $this->tables ) ) {
309
- $this->log( "Preparing Data Step2: Skipping" );
310
- return true;
311
- }
312
-
313
- $result = $this->db->query(
314
- $this->db->prepare(
315
- "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'wpstg_is_staging_site'", "true"
316
- )
317
- );
318
-
319
- // No errors but no option name such as wpstg_is_staging_site
320
- if( '' === $this->db->last_error && 0 == $result ) {
321
- $result = $this->db->query(
322
- $this->db->prepare(
323
- "INSERT INTO {$this->prefix}options (option_name,option_value) VALUES ('wpstg_is_staging_site',%s)", "true"
324
- )
325
- );
326
- }
327
-
328
- // All good
329
- if( $result ) {
330
- return true;
331
- }
332
-
333
- $this->log( "Preparing Data Step2: Failed to update wpstg_is_staging_site in {$this->prefix}options {$this->db->last_error}", Logger::TYPE_ERROR );
334
- return false;
335
- }
336
-
337
- /**
338
- * Update rewrite_rules
339
- * @return bool
340
- */
341
- protected function step3() {
342
-
343
- $this->log( "Preparing Data Step3: Updating rewrite_rules in {$this->prefix}options {$this->db->last_error}" );
344
-
345
- // Skip - Table does not exist
346
- if( false === $this->isTable( $this->prefix . 'options' ) ) {
347
- return true;
348
- }
349
-
350
- // Skip - Table is not selected or updated
351
- if( !in_array( $this->prefix . 'options', $this->tables ) ) {
352
- $this->log( "Preparing Data Step3: Skipping" );
353
- return true;
354
- }
355
-
356
- $result = $this->db->query(
357
- $this->db->prepare(
358
- "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'rewrite_rules'", ' '
359
- )
360
- );
361
-
362
- // All good
363
- if( $result ) {
364
- return true;
365
- }
366
-
367
- $this->log( "Preparing Data Step3: Failed to update rewrite_rules in {$this->prefix}options {$this->db->last_error}", Logger::TYPE_ERROR );
368
- return true;
369
- }
370
-
371
- /**
372
- * Update Table Prefix in wp_usermeta
373
- * @return bool
374
- */
375
- protected function step4() {
376
- $this->log( "Preparing Data Step4: Updating db prefix in {$this->prefix}usermeta. " );
377
-
378
- // Skip - Table does not exist
379
- if( false === $this->isTable( $this->prefix . 'usermeta' ) ) {
380
- return true;
381
- }
382
-
383
- // Skip - Table is not selected or updated
384
- if( !in_array( $this->prefix . 'usermeta', $this->tables ) ) {
385
- $this->log( "Preparing Data Step4: Skipping" );
386
- return true;
387
- }
388
-
389
- // Skip, prefixes are identical. No change needed
390
- // if( $this->db->prefix === $this->prefix ) {
391
- // $this->log( "Preparing Data Step4: Skipping" );
392
- // return true;
393
- // }
394
-
395
- $update = $this->db->query(
396
- $this->db->prepare(
397
- "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 . "_%"
398
- )
399
- );
400
-
401
- if( !$update ) {
402
- $this->log( "Preparing Data Step4a: Skip updating {$this->prefix}usermeta meta_key database base_prefix; {$this->db->last_error}", Logger::TYPE_INFO );
403
- //return true;
404
- }
405
-
406
- $update = $this->db->query(
407
- $this->db->prepare(
408
- "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 . "_%"
409
- )
410
- );
411
-
412
- if( !$update ) {
413
- $this->log( "Preparing Data Step4: Skip updating {$this->prefix}usermeta meta_key database table prefixes; {$this->db->last_error}", Logger::TYPE_INFO );
414
- //$this->returnException( "Data Crunching Step 4: Failed to update {$this->prefix}usermeta meta_key database table prefixes; {$this->db->last_error}" );
415
- return true;
416
- }
417
- return true;
418
- }
419
-
420
- /**
421
- * Update $table_prefix in wp-config.php
422
- * @return bool
423
- */
424
- protected function step5() {
425
- $path = $this->options->destinationDir . "wp-config.php";
426
-
427
- $this->log( "Preparing Data Step5: Updating table_prefix in {$path} to " . $this->prefix );
428
- if( false === ($content = file_get_contents( $path )) ) {
429
- $this->log( "Preparing Data Step5: Failed to update table_prefix in {$path}. Can't read contents", Logger::TYPE_ERROR );
430
- return false;
431
- }
432
-
433
- // Replace table prefix
434
- $content = str_replace( '$table_prefix', '$table_prefix = \'' . $this->prefix . '\';//', $content );
435
-
436
- // Replace URLs
437
- $content = str_replace( $this->multisiteHomeDomain, $this->getStagingSiteUrl(), $content );
438
-
439
- if( false === @file_put_contents( $path, $content ) ) {
440
- $this->log( "Preparing Data Step5: Failed to update $table_prefix in {$path} to " . $this->prefix . ". Can't save contents", Logger::TYPE_ERROR );
441
- return false;
442
- }
443
-
444
- return true;
445
- }
446
-
447
- /**
448
- * Reset index.php to original file
449
- * This is needed if live site is located in subfolder
450
- * Check first if main wordpress is used in subfolder and index.php in parent directory
451
- * @see: https://codex.wordpress.org/Giving_WordPress_Its_Own_Directory
452
- * @return bool
453
- */
454
- protected function step6() {
455
-
456
- if( !$this->isSubDir() ) {
457
- $this->debugLog( "Preparing Data Step6: WP installation is not in a subdirectory! All good, skipping this step" );
458
- return true;
459
- }
460
-
461
- $path = $this->options->destinationDir . "index.php";
462
-
463
- if( false === ($content = file_get_contents( $path )) ) {
464
- $this->log( "Preparing Data Step6: Failed to reset {$path} for sub directory; can't read contents", Logger::TYPE_ERROR );
465
- return false;
466
- }
467
-
468
-
469
- if( !preg_match( "/(require(.*)wp-blog-header.php' \);)/", $content, $matches ) ) {
470
- $this->log(
471
- "Preparing Data Step6: Failed to reset index.php for sub directory; wp-blog-header.php is missing", Logger::TYPE_ERROR
472
- );
473
- return false;
474
- }
475
- $this->log( "Preparing Data: WP installation is in a subdirectory. Progressing..." );
476
-
477
- $pattern = "/require(.*) dirname(.*) __FILE__ (.*) \. '(.*)wp-blog-header.php'(.*);/";
478
-
479
- $replace = "require( dirname( __FILE__ ) . '/wp-blog-header.php' ); // " . $matches[0];
480
- $replace.= " // Changed by WP-Staging";
481
-
482
-
483
-
484
- if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
485
- $this->log( "Preparing Data: Failed to reset index.php for sub directory; replacement failed", Logger::TYPE_ERROR );
486
- return false;
487
- }
488
-
489
- if( false === @file_put_contents( $path, $content ) ) {
490
- $this->log( "Preparing Data: Failed to reset index.php for sub directory; can't save contents", Logger::TYPE_ERROR );
491
- return false;
492
- }
493
- $this->Log( "Preparing Data: Finished Step 6 successfully" );
494
- return true;
495
- }
496
-
497
- /**
498
- * Update wpstg_rmpermalinks_executed
499
- * @return bool
500
- */
501
- protected function step7() {
502
-
503
- $this->log( "Preparing Data Step7: Updating wpstg_rmpermalinks_executed in {$this->prefix}options {$this->db->last_error}" );
504
-
505
- // Skip - Table does not exist
506
- if( false === $this->isTable( $this->prefix . 'options' ) ) {
507
- return true;
508
- }
509
-
510
- // Skip - Table is not selected or updated
511
- if( !in_array( $this->prefix . 'options', $this->tables ) ) {
512
- $this->log( "Preparing Data Step7: Skipping" );
513
- return true;
514
- }
515
-
516
- $result = $this->db->query(
517
- $this->db->prepare(
518
- "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'wpstg_rmpermalinks_executed'", ' '
519
- )
520
- );
521
-
522
- // All good
523
- if( $result ) {
524
- $this->Log( "Preparing Data Step7: Finished Step 7 successfully" );
525
- return true;
526
- }
527
-
528
- $this->log( "Failed to update wpstg_rmpermalinks_executed in {$this->prefix}options {$this->db->last_error}", Logger::TYPE_WARNING );
529
- return true;
530
- }
531
-
532
- /**
533
- * Update permalink_structure
534
- * @return bool
535
- */
536
- protected function step8() {
537
-
538
- $this->log( "Preparing Data Step8: Updating permalink_structure in {$this->prefix}options {$this->db->last_error}" );
539
-
540
- // Skip - Table does not exist
541
- if( false === $this->isTable( $this->prefix . 'options' ) ) {
542
- return true;
543
- }
544
-
545
- // Skip - Table is not selected or updated
546
- if( !in_array( $this->prefix . 'options', $this->tables ) ) {
547
- $this->log( "Preparing Data Step8: Skipping" );
548
- return true;
549
- }
550
-
551
- $result = $this->db->query(
552
- $this->db->prepare(
553
- "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'permalink_structure'", ' '
554
- )
555
- );
556
-
557
- // All good
558
- if( $result ) {
559
- $this->Log( "Preparing Data Step8: Finished Step 8 successfully" );
560
- return true;
561
- }
562
-
563
- $this->log( "Failed to update permalink_structure in {$this->prefix}options {$this->db->last_error}", Logger::TYPE_ERROR );
564
- return true;
565
- }
566
-
567
- /**
568
- * Update blog_public option to not allow staging site to be indexed by search engines
569
- * @return bool
570
- */
571
- protected function step9() {
572
-
573
- $this->log( "Preparing Data Step9: Set staging site to noindex" );
574
-
575
- if( false === $this->isTable( $this->prefix . 'options' ) ) {
576
- return true;
577
- }
578
-
579
- // Skip - Table is not selected or updated
580
- if( !in_array( $this->prefix . 'options', $this->tables ) ) {
581
- $this->log( "Preparing Data Step9: Skipping" );
582
- return true;
583
- }
584
-
585
- $result = $this->db->query(
586
- $this->db->prepare(
587
- "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'blog_public'", '0'
588
- )
589
- );
590
-
591
- // All good
592
- if( $result ) {
593
- $this->Log( "Preparing Data Step9: Finished Step 9 successfully" );
594
- return true;
595
- }
596
-
597
- $this->log( "Can not update staging site to noindex. Possible already done!", Logger::TYPE_WARNING );
598
- return true;
599
- }
600
-
601
- /**
602
- * Update WP_HOME in wp-config.php
603
- * @return bool
604
- */
605
- protected function step10() {
606
- $path = $this->options->destinationDir . "wp-config.php";
607
-
608
- $this->log( "Preparing Data Step10: Updating WP_HOME in wp-config.php to " . $this->getStagingSiteUrl() );
609
-
610
- if( false === ($content = file_get_contents( $path )) ) {
611
- $this->log( "Preparing Data Step10: Failed to update WP_HOME in wp-config.php. Can't read wp-config.php", Logger::TYPE_ERROR );
612
- return false;
613
- }
614
-
615
-
616
- // Get WP_HOME from wp-config.php
617
- preg_match( "/define\s*\(\s*'WP_HOME'\s*,\s*(.*)\s*\);/", $content, $matches );
618
-
619
- if( !empty( $matches[1] ) ) {
620
- $matches[1];
621
-
622
- $pattern = "/define\s*\(\s*'WP_HOME'\s*,\s*(.*)\s*\);/";
623
-
624
- $replace = "define('WP_HOME','" . $this->getStagingSiteUrl() . "'); // " . $matches[1];
625
- $replace.= " // Changed by WP-Staging";
626
-
627
- if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
628
- $this->log( "Preparing Data: Failed to update WP_HOME", Logger::TYPE_ERROR );
629
- return false;
630
- }
631
- } else {
632
- $this->log( "Preparing Data Step10: WP_HOME not defined in wp-config.php. Skipping this step." );
633
- }
634
-
635
- if( false === @file_put_contents( $path, $content ) ) {
636
- $this->log( "Preparing Data Step10: Failed to update WP_HOME. Can't save contents", Logger::TYPE_ERROR );
637
- return false;
638
- }
639
- $this->Log( "Preparing Data: Finished Step 10 successfully" );
640
- return true;
641
- }
642
-
643
- /**
644
- * Update WP_SITEURL in wp-config.php
645
- * @return bool
646
- */
647
- protected function step11() {
648
- $path = $this->options->destinationDir . "wp-config.php";
649
-
650
- $this->log( "Preparing Data Step11: Updating WP_SITEURL in wp-config.php to " . $this->getStagingSiteUrl() );
651
-
652
- if( false === ($content = file_get_contents( $path )) ) {
653
- $this->log( "Preparing Data Step11: Failed to update WP_SITEURL in wp-config.php. Can't read wp-config.php", Logger::TYPE_ERROR );
654
- return false;
655
- }
656
-
657
-
658
- // Get WP_SITEURL from wp-config.php
659
- preg_match( "/define\s*\(\s*'WP_SITEURL'\s*,\s*(.*)\s*\);/", $content, $matches );
660
-
661
- if( !empty( $matches[1] ) ) {
662
- $matches[1];
663
-
664
- $pattern = "/define\s*\(\s*'WP_SITEURL'\s*,\s*(.*)\s*\);/";
665
-
666
- $replace = "define('WP_SITEURL','" . $this->getStagingSiteUrl() . "'); // " . $matches[1];
667
- $replace.= " // Changed by WP-Staging";
668
-
669
- if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
670
- $this->log( "Preparing Data Step11: Failed to update WP_SITEURL", Logger::TYPE_ERROR );
671
- return false;
672
- }
673
- } else {
674
- $this->log( "Preparing Data Step11: WP_SITEURL not defined in wp-config.php. Skipping this step." );
675
- }
676
-
677
-
678
- if( false === @file_put_contents( $path, $content ) ) {
679
- $this->log( "Preparing Data Step11: Failed to update WP_SITEURL. Can't save contents", Logger::TYPE_ERROR );
680
- return false;
681
- }
682
- $this->Log( "Preparing Data: Finished Step 11 successfully" );
683
- return true;
684
- }
685
-
686
- /**
687
- * Update WP_ALLOW_MULTISITE constant in wp-config.php
688
- * @return bool
689
- */
690
- protected function step12() {
691
- $path = $this->options->destinationDir . "wp-config.php";
692
-
693
- $this->log( "Preparing Data Step12: Updating WP_ALLOW_MULTISITE in wp-config.php to false" );
694
-
695
- if( false === ($content = file_get_contents( $path )) ) {
696
- $this->log( "Preparing Data Step12: Failed to update WP_ALLOW_MULTISITE in wp-config.php. Can't read wp-config.php", Logger::TYPE_ERROR );
697
- return false;
698
- }
699
-
700
-
701
- // Get WP_SITEURL from wp-config.php
702
- preg_match( "/define\s*\(\s*'WP_ALLOW_MULTISITE'\s*,\s*(.*)\s*\);/", $content, $matches );
703
-
704
- if( !empty( $matches[1] ) ) {
705
- $matches[1];
706
-
707
- $pattern = "/define\s*\(\s*'WP_ALLOW_MULTISITE'\s*,\s*(.*)\s*\);/";
708
-
709
- $replace = "define('WP_ALLOW_MULTISITE',false); // " . $matches[1];
710
- $replace.= " // Changed by WP-Staging";
711
-
712
- if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
713
- $this->log( "Preparing Data Step12: Failed to update WP_ALLOW_MULTISITE", Logger::TYPE_ERROR );
714
- return false;
715
- }
716
- } else {
717
- $this->log( "Preparing Data Step12: WP_ALLOW_MULTISITE not defined in wp-config.php. Skipping this step." );
718
- }
719
-
720
-
721
- if( false === @file_put_contents( $path, $content ) ) {
722
- $this->log( "Preparing Data Step12: Failed to update WP_ALLOW_MULTISITE. Can't save contents", Logger::TYPE_ERROR );
723
- return false;
724
- }
725
- $this->Log( "Preparing Data: Finished Step 12 successfully" );
726
- return true;
727
- }
728
-
729
- /**
730
- * Update MULTISITE constant in wp-config.php
731
- * @return bool
732
- */
733
- protected function step13() {
734
- $path = $this->options->destinationDir . "wp-config.php";
735
-
736
- $this->log( "Preparing Data Step13: Updating MULTISITE in wp-config.php to false" );
737
-
738
- if( false === ($content = file_get_contents( $path )) ) {
739
- $this->log( "Preparing Data Step13: Failed to update MULTISITE in wp-config.php. Can't read wp-config.php", Logger::TYPE_ERROR );
740
- return false;
741
- }
742
-
743
-
744
- // Get WP_SITEURL from wp-config.php
745
- preg_match( "/define\s*\(\s*'MULTISITE'\s*,\s*(.*)\s*\);/", $content, $matches );
746
-
747
- if( !empty( $matches[1] ) ) {
748
- $matches[1];
749
-
750
- $pattern = "/define\s*\(\s*'MULTISITE'\s*,\s*(.*)\s*\);/";
751
-
752
- $replace = "define('MULTISITE',false); // " . $matches[1];
753
- $replace.= " // Changed by WP-Staging";
754
-
755
- if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
756
- $this->log( "Preparing Data Step13: Failed to update MULTISITE", Logger::TYPE_ERROR );
757
- return false;
758
- }
759
- } else {
760
- $this->log( "Preparing Data Step13: MULTISITE not defined in wp-config.php. Skipping this step." );
761
- }
762
-
763
-
764
- if( false === @file_put_contents( $path, $content ) ) {
765
- $this->log( "Preparing Data Step13: Failed to update MULTISITE. Can't save contents", Logger::TYPE_ERROR );
766
- return false;
767
- }
768
- $this->Log( "Preparing Data: Finished Step 13 successfully" );
769
- return true;
770
- }
771
-
772
- /**
773
- * Get active_sitewide_plugins from wp_sitemeta and active_plugins from subsite
774
- * Merge both arrays and copy them to the staging site into active_plugins
775
- */
776
- protected function step14() {
777
-
778
-
779
- $this->log( "Data Crunching Step 14: Updating active_plugins" );
780
-
781
- if( false === $this->isTable( $this->prefix . 'options' ) ) {
782
- $this->log( 'Data Crunching Step 14: Fatal Error ' . $this->prefix . 'options does not exist' );
783
- $this->returnException( 'Data Crunching Step 14: Fatal Error ' . $this->prefix . 'options does not exist' );
784
- return false;
785
- }
786
-
787
- // Skip - Table is not selected or updated
788
- if( !in_array( $this->prefix . 'options', $this->tables ) ) {
789
- $this->log( "Preparing Data Step14: Skipping" );
790
- return true;
791
- }
792
-
793
- // Get active_plugins value from sub site options table
794
- $active_plugins = $this->productionDb->get_var( "SELECT option_value FROM {$this->productionDb->prefix}options WHERE option_name = 'active_plugins' " );
795
-
796
- if( !$active_plugins ) {
797
- $this->log( "Data Crunching Step 14: Option active_plugins are empty " );
798
- $active_plugins = array();
799
- }
800
- // Get active_sitewide_plugins value from main multisite wp_sitemeta table
801
- $active_sitewide_plugins = $this->productionDb->get_var( "SELECT meta_value FROM {$this->productionDb->base_prefix}sitemeta WHERE meta_key = 'active_sitewide_plugins' " );
802
-
803
- if( !$active_sitewide_plugins ) {
804
- $this->log( "Data Crunching Step 14: Options {$this->productionDb->base_prefix}active_sitewide_plugins is empty " );
805
- $active_sitewide_plugins = array();
806
- }
807
-
808
- $active_sitewide_plugins = unserialize( $active_sitewide_plugins );
809
- $active_plugins = unserialize( $active_plugins );
810
-
811
- $all_plugins = array_merge( $active_plugins, array_keys( $active_sitewide_plugins ) );
812
-
813
- sort( $all_plugins );
814
-
815
-
816
- // Update active_plugins
817
- $update = $this->db->query(
818
- "UPDATE {$this->prefix}options SET option_value = '" . serialize( $all_plugins ) . "' WHERE option_name = 'active_plugins'"
819
- );
820
-
821
- if( false === $update ) {
822
- $this->log( "Data Crunching Step 14: Can not update option active_plugins in {$this->prefix}options", Logger::TYPE_WARNING );
823
- return false;
824
- }
825
-
826
- $this->log( "Data Crunching Step 14: Successfull!" );
827
- return true;
828
- }
829
-
830
- /**
831
- * Update Table Prefix in wp_options
832
- * @return bool
833
- */
834
- protected function step15() {
835
- $this->log( "Preparing Data Step15: Updating db prefix in {$this->prefix}options." );
836
-
837
- // Skip - Table does not exist
838
- if( false === $this->isTable( $this->prefix . 'options' ) ) {
839
- return true;
840
- }
841
-
842
- // Skip - Table is not selected or updated
843
- if( !in_array( $this->prefix . 'options', $this->tables ) ) {
844
- $this->log( "Preparing Data Step4: Skipping" );
845
- return true;
846
- }
847
-
848
- // Skip, prefixes are identical. No change needed
849
- // if( $this->productionDb->prefix === $this->prefix ) {
850
- // $this->log( "Preparing Data Step4: Skipping" );
851
- // return true;
852
- // }
853
-
854
- $this->log( "Updating db option_names in {$this->prefix}options. " );
855
-
856
- // Filter the rows below. Do not update them!
857
- $filters = array(
858
- 'wp_mail_smtp',
859
- 'wp_mail_smtp_version',
860
- 'wp_mail_smtp_debug',
861
- );
862
-
863
- $filters = apply_filters( 'wpstg_data_excl_rows', $filters );
864
-
865
- $where = "";
866
- foreach ( $filters as $filter ) {
867
- $where .= " AND option_name <> '" . $filter . "'";
868
- }
869
-
870
- $updateOptions = $this->db->query(
871
- $this->db->prepare(
872
- "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 . "_%"
873
- )
874
- );
875
-
876
- if( !$updateOptions ) {
877
- $this->log( "Preparing Data Step15: Skip updating db option_names in {$this->prefix}options. Error: {$this->db->last_error}", Logger::TYPE_WARNING );
878
- //$this->returnException( "Data Crunching Step 15: Failed to update db option_names in {$this->prefix}options. Error: {$this->db->last_error}" );
879
- return true;
880
- }
881
-
882
-
883
- return true;
884
- }
885
-
886
- /**
887
- * Change upload_path in wp_options (if it is defined)
888
- * @return bool
889
- */
890
- protected function step16() {
891
- $this->log( "Preparing Data Step16: Updating upload_path {$this->prefix}options." );
892
-
893
- // Skip - Table does not exist
894
- if( false === $this->isTable( $this->prefix . 'options' ) ) {
895
- return true;
896
- }
897
-
898
- $newUploadPath = $this->getNewUploadPath();
899
-
900
- if( false === $newUploadPath ) {
901
- $this->log( "Preparing Data Step16: Skipping" );
902
- return true;
903
- }
904
-
905
- // Skip - Table is not selected or updated
906
- if( !in_array( $this->prefix . 'options', $this->tables ) ) {
907
- $this->log( "Preparing Data Step16: Skipping" );
908
- return true;
909
- }
910
-
911
- $error = isset( $this->db->last_error ) ? 'Last error: ' . $this->db->last_error : '';
912
-
913
- $this->log( "Updating upload_path in {$this->prefix}options. {$error}" );
914
-
915
- $updateOptions = $this->db->query(
916
- $this->db->prepare(
917
- "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'upload_path'", $newUploadPath
918
- )
919
- );
920
-
921
- if( !$updateOptions ) {
922
- $this->log( "Preparing Data Step16: Failed to update upload_path in {$this->prefix}options. {$error}", Logger::TYPE_ERROR );
923
- return true;
924
- }
925
- $this->Log( "Preparing Data: Finished Step 16 successfully" );
926
- return true;
927
- }
928
-
929
- /**
930
- * Update WP_CACHE in wp-config.php
931
- * @return bool
932
- */
933
- protected function step17() {
934
- $path = $this->options->destinationDir . "wp-config.php";
935
-
936
- $this->log( "Preparing Data Step17: Set WP_CACHE in wp-config.php to false" );
937
-
938
- if( false === ($content = file_get_contents( $path )) ) {
939
- $this->log( "Preparing Data Step17: Failed to update WP_CACHE in wp-config.php. Can't read wp-config.php", Logger::TYPE_ERROR );
940
- return false;
941
- }
942
-
943
-
944
- // Get WP_CACHE from wp-config.php
945
- preg_match( "/define\s*\(\s*'WP_CACHE'\s*,\s*(.*)\s*\);/", $content, $matches );
946
-
947
- if( !empty( $matches[1] ) ) {
948
- $matches[1];
949
-
950
- $pattern = "/define\s*\(\s*'WP_CACHE'\s*,\s*(.*)\s*\);/";
951
-
952
- $replace = "define('WP_CACHE',false); // " . $matches[1];
953
- $replace.= " // Changed by WP-Staging";
954
-
955
- if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
956
- $this->log( "Preparing Data: Failed to change WP_CACHE", Logger::TYPE_ERROR );
957
- return false;
958
- }
959
- } else {
960
- $this->log( "Preparing Data Step17: WP_CACHE not defined in wp-config.php. Skipping this step." );
961
- }
962
-
963
- if( false === @file_put_contents( $path, $content ) ) {
964
- $this->log( "Preparing Data Step17: Failed to update WP_CACHE. Can't save contents", Logger::TYPE_ERROR );
965
- return false;
966
- }
967
- $this->Log( "Preparing Data: Finished Step 17 successfully" );
968
- return true;
969
- }
970
-
971
- /**
972
- * Update database credentials in wp-config.php
973
- * @return bool
974
- */
975
- protected function step18() {
976
- $path = $this->options->destinationDir . "wp-config.php";
977
-
978
- $this->log( "Preparing Data Step18: Change database credentials in wp-config.php" );
979
-
980
- if( false === ($content = file_get_contents( $path )) ) {
981
- $this->log( "Preparing Data Step18: Failed to update database credentials in wp-config.php. Can't read wp-config.php", Logger::TYPE_ERROR );
982
- return false;
983
- }
984
-
985
-
986
- // Get DB_NAME from wp-config.php
987
- preg_match( "/define\s*\(\s*'DB_NAME'\s*,\s*(.*)\s*\);/", $content, $matches );
988
-
989
- if( !empty( $matches[1] ) ) {
990
- $matches[1];
991
-
992
- $pattern = "/define\s*\(\s*'DB_NAME'\s*,\s*(.*)\s*\);/";
993
-
994
- $replace = "define('DB_NAME','{$this->options->databaseDatabase}'); // " . $matches[1];
995
- $replace.= " // Changed by WP-Staging";
996
-
997
- if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
998
- $this->log( "Preparing Data: Failed to change DB_NAME", Logger::TYPE_ERROR );
999
- return false;
1000
- }
1001
- } else {
1002
- $this->log( "Preparing Data Step18: DB_NAME not defined in wp-config.php. Skipping this step." );
1003
- }
1004
- // Get DB_USER from wp-config.php
1005
- preg_match( "/define\s*\(\s*'DB_USER'\s*,\s*(.*)\s*\);/", $content, $matches );
1006
-
1007
- if( !empty( $matches[1] ) ) {
1008
- $matches[1];
1009
-
1010
- $pattern = "/define\s*\(\s*'DB_USER'\s*,\s*(.*)\s*\);/";
1011
-
1012
- $replace = "define('DB_USER','{$this->options->databaseUser}'); // " . $matches[1];
1013
- $replace.= " // Changed by WP-Staging";
1014
-
1015
- if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
1016
- $this->log( "Preparing Data: Failed to change DB_USER", Logger::TYPE_ERROR );
1017
- return false;
1018
- }
1019
- } else {
1020
- $this->log( "Preparing Data Step18: DB_USER not defined in wp-config.php. Skipping this step." );
1021
- }
1022
- // Get DB_PASSWORD from wp-config.php
1023
- preg_match( "/define\s*\(\s*'DB_PASSWORD'\s*,\s*(.*)\s*\);/", $content, $matches );
1024
-
1025
- if( !empty( $matches[1] ) ) {
1026
- $matches[1];
1027
-
1028
- $pattern = "/define\s*\(\s*'DB_PASSWORD'\s*,\s*(.*)\s*\);/";
1029
-
1030
- $replace = "define('DB_PASSWORD','{$this->options->databasePassword}'); // " . $matches[1];
1031
- $replace.= " // Changed by WP-Staging";
1032
-
1033
- if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
1034
- $this->log( "Preparing Data: Failed to change DB_PASSWORD", Logger::TYPE_ERROR );
1035
- return false;
1036
- }
1037
- } else {
1038
- $this->log( "Preparing Data Step18: DB_PASSWORD not defined in wp-config.php. Skipping this step." );
1039
- }
1040
- // Get DB_HOST from wp-config.php
1041
- preg_match( "/define\s*\(\s*'DB_HOST'\s*,\s*(.*)\s*\);/", $content, $matches );
1042
-
1043
- if( !empty( $matches[1] ) ) {
1044
- $matches[1];
1045
-
1046
- $pattern = "/define\s*\(\s*'DB_HOST'\s*,\s*(.*)\s*\);/";
1047
-
1048
- $replace = "define('DB_HOST','{$this->options->databaseServer}'); // " . $matches[1];
1049
- $replace.= " // Changed by WP-Staging";
1050
-
1051
- if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
1052
- $this->log( "Preparing Data: Failed to change DB_HOST", Logger::TYPE_ERROR );
1053
- return false;
1054
- }
1055
- } else {
1056
- $this->log( "Preparing Data Step18: DB_HOST not defined in wp-config.php. Skipping this step." );
1057
- }
1058
-
1059
-
1060
- if( false === @file_put_contents( $path, $content ) ) {
1061
- $this->log( "Preparing Data Step18: Failed to update database credentials in wp-config.php. Can't save contents", Logger::TYPE_ERROR );
1062
- return false;
1063
- }
1064
- $this->Log( "Preparing Data: Finished Step 18 successfully" );
1065
- return true;
1066
- }
1067
-
1068
- /**
1069
- * Get upload path
1070
- * @return boolean|string
1071
- */
1072
- protected function getNewUploadPath() {
1073
- $uploadPath = get_option( 'upload_path' );
1074
-
1075
- if( !$uploadPath ) {
1076
- return false;
1077
- }
1078
-
1079
- $customSlug = str_replace( \WPStaging\WPStaging::getWPpath(), '', $uploadPath );
1080
-
1081
- $newUploadPath = \WPStaging\WPStaging::getWPpath() . $this->options->cloneDirectoryName . DIRECTORY_SEPARATOR . $customSlug;
1082
-
1083
- return $newUploadPath;
1084
- }
1085
-
1086
- /**
1087
- * Return URL to staging site
1088
- * @return string
1089
- */
1090
- protected function getStagingSiteUrl() {
1091
-
1092
- if( !empty( $this->options->cloneHostname ) ) {
1093
- return $this->options->cloneHostname;
1094
- }
1095
-
1096
- if( $this->isSubDir() ) {
1097
- return trailingslashit( $this->multisiteHomeDomain) . trailingslashit( $this->getSubDir() ) . $this->options->cloneDirectoryName;
1098
- }
1099
-
1100
- return trailingslashit( $this->multisiteHomeDomain) . $this->options->cloneDirectoryName;
1101
- }
1102
-
1103
- /**
1104
- * Check if WP is installed in subdir
1105
- * @return boolean
1106
- */
1107
- protected function isSubDir() {
1108
- // Compare names without scheme to bypass cases where siteurl and home have different schemes http / https
1109
- // This is happening much more often than you would expect
1110
- $siteurl = preg_replace( '#^https?://#', '', rtrim( get_option( 'siteurl' ), '/' ) );
1111
- $home = preg_replace( '#^https?://#', '', rtrim( get_option( 'home' ), '/' ) );
1112
-
1113
- if( $home !== $siteurl ) {
1114
- return true;
1115
- }
1116
- return false;
1117
- }
1118
-
1119
- /**
1120
- * Get the install sub directory if WP is installed in sub directory
1121
- * @return string
1122
- */
1123
- protected function getSubDir() {
1124
- $home = get_option( 'home' );
1125
- $siteurl = get_option( 'siteurl' );
1126
-
1127
- if( empty( $home ) || empty( $siteurl ) ) {
1128
- return '';
1129
- }
1130
-
1131
- $dir = str_replace( $home, '', $siteurl );
1132
- return str_replace( '/', '', $dir );
1133
- }
1134
-
1135
- }
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, $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 = 19;
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
+ $this->options->currentStep > $this->options->totalSteps ||
145
+ !method_exists( $this, "step" . $this->options->currentStep )
146
+ );
147
+ }
148
+
149
+ /**
150
+ * Check if current operation is done on the root folder or on the live DB
151
+ * @return boolean
152
+ */
153
+ protected function isRoot() {
154
+
155
+ // Prefix is the same as the one of live site
156
+ // $wpdb = WPStaging::getInstance()->get( "wpdb" );
157
+ // if( $wpdb->prefix === $this->prefix ) {
158
+ // return true;
159
+ // }
160
+ // CloneName is empty
161
+ $name = ( array ) $this->options->cloneDirectoryName;
162
+ if( empty( $name ) ) {
163
+ return true;
164
+ }
165
+
166
+ // Live domain === Staging domain
167
+ if( $this->multisiteHomeDomain . $this->options->cloneDirectoryName === $this->multisiteHomeDomain ) {
168
+ return true;
169
+ }
170
+
171
+ return false;
172
+ }
173
+
174
+ /**
175
+ * Check if table exists
176
+ * @param string $table
177
+ * @return boolean
178
+ */
179
+ protected function isTable( $table ) {
180
+ if( $this->db->get_var( "SHOW TABLES LIKE '{$table}'" ) != $table ) {
181
+ $this->log( "Table {$table} does not exists", Logger::TYPE_ERROR );
182
+ return false;
183
+ }
184
+ return true;
185
+ }
186
+
187
+ /**
188
+ * Return absolute destination path
189
+ * @return string
190
+ */
191
+ // private function getAbsDestination() {
192
+ // if( empty( $this->options->cloneDir ) ) {
193
+ // return \WPStaging\WPStaging::getWPpath();
194
+ // }
195
+ // return trailingslashit( $this->options->cloneDir );
196
+ // }
197
+
198
+ /**
199
+ * Copy wp-config.php if it is located outside of root one level up
200
+ * @todo Needs some more testing before it will be released
201
+ * @return boolean
202
+ */
203
+ protected function step0() {
204
+ $this->log( "Preparing Data Step0: Check if wp-config.php is located in root path", Logger::TYPE_INFO );
205
+
206
+ $dir = trailingslashit( dirname( ABSPATH ) );
207
+
208
+ $source = $dir . 'wp-config.php';
209
+
210
+ $destination = $this->options->destinationDir . 'wp-config.php';
211
+
212
+
213
+ // Do not do anything
214
+ if( (!is_file( $source ) && !is_link( $source )) || is_file( $destination ) ) {
215
+ $this->log( "Preparing Data Step0: Skip it", Logger::TYPE_INFO );
216
+ return true;
217
+ }
218
+
219
+ // Copy target of a symbolic link
220
+ if( is_link( $source ) ) {
221
+ $this->log( "Preparing Data Step0: Symbolic link found...", Logger::TYPE_INFO );
222
+ if( !@copy( readlink( $source ), $destination ) ) {
223
+ $errors = error_get_last();
224
+ $this->log( "Preparing Data Step0: Failed to copy wp-config.php! Error: {$errors['message']} {$source} -> {$destination}", Logger::TYPE_ERROR );
225
+ return true;
226
+ }
227
+ }
228
+
229
+ // Copy file wp-config.php
230
+ if( !@copy( $source, $destination ) ) {
231
+ $errors = error_get_last();
232
+ $this->log( "Preparing Data Step0: Failed to copy wp-config.php! Error: {$errors['message']} {$source} -> {$destination}", Logger::TYPE_ERROR );
233
+ return true;
234
+ }
235
+ $this->log( "Preparing Data Step0: Successfull", Logger::TYPE_INFO );
236
+ return true;
237
+ }
238
+
239
+ /**
240
+ * Replace "siteurl" and "home"
241
+ * @return bool
242
+ */
243
+ protected function step1() {
244
+ $this->log( "Preparing Data Step1: Updating siteurl and homeurl in {$this->prefix}options {$this->db->last_error}", Logger::TYPE_INFO );
245
+
246
+ // Skip - Table does not exist
247
+ if( false === $this->isTable( $this->prefix . 'options' ) ) {
248
+ return true;
249
+ }
250
+ // Skip - Table is not selected or updated
251
+ if( !in_array( $this->prefix . 'options', $this->tables ) ) {
252
+ $this->log( "Preparing Data Step1: Skipping" );
253
+ return true;
254
+ }
255
+
256
+ // Installed in sub-directory
257
+ // if( $this->isSubDir() ) {
258
+ // $this->log( "Preparing Data Step1: Updating siteurl and homeurl to " . rtrim( $this->multisiteHomeDomain, "/" ) . '/' . $this->options->cloneDirectoryName );
259
+ // // Replace URLs
260
+ // $result = $this->db->query(
261
+ // $this->db->prepare(
262
+ // "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'siteurl' or option_name='home'", rtrim( $this->multisiteHomeDomain, "/" ) . '/' . $this->options->cloneDirectoryName
263
+ // )
264
+ // );
265
+ // } else
266
+ // {
267
+ // $this->log( "Preparing Data Step1: Updating siteurl and homeurl to " . rtrim( $this->multisiteHomeDomain, "/" ) . '/' . $this->options->cloneDirectoryName );
268
+ // // Replace URLs
269
+ // $result = $this->db->query(
270
+ // $this->db->prepare(
271
+ // "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'siteurl' or option_name='home'", $this->multisiteHomeDomain . '/' . $this->options->cloneDirectoryName
272
+ // )
273
+ // );
274
+ // }
275
+
276
+ $this->log( "Preparing Data Step1: Updating siteurl and homeurl to " . $this->getStagingSiteUrl() );
277
+ // Replace URLs
278
+ $result = $this->db->query(
279
+ $this->db->prepare(
280
+ "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'siteurl' or option_name='home'", $this->getStagingSiteUrl()
281
+ )
282
+ );
283
+
284
+
285
+
286
+ // All good
287
+ if( $result ) {
288
+ return true;
289
+ }
290
+
291
+ $this->log( "Preparing Data Step1: Failed to update siteurl and homeurl in {$this->prefix}options {$this->db->last_error}", Logger::TYPE_ERROR );
292
+ return false;
293
+ }
294
+
295
+ /**
296
+ * Update "wpstg_is_staging_site"
297
+ * @return bool
298
+ */
299
+ protected function step2() {
300
+
301
+ $this->log( "Preparing Data Step2: Updating row wpstg_is_staging_site in {$this->prefix}options {$this->db->last_error}" );
302
+
303
+ // Skip - Table does not exist
304
+ if( false === $this->isTable( $this->prefix . 'options' ) ) {
305
+ return true;
306
+ }
307
+ // Skip - Table is not selected or updated
308
+ if( !in_array( $this->prefix . 'options', $this->tables ) ) {
309
+ $this->log( "Preparing Data Step2: Skipping" );
310
+ return true;
311
+ }
312
+
313
+ $result = $this->db->query(
314
+ $this->db->prepare(
315
+ "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'wpstg_is_staging_site'", "true"
316
+ )
317
+ );
318
+
319
+ // No errors but no option name such as wpstg_is_staging_site
320
+ if( '' === $this->db->last_error && 0 == $result ) {
321
+ $result = $this->db->query(
322
+ $this->db->prepare(
323
+ "INSERT INTO {$this->prefix}options (option_name,option_value) VALUES ('wpstg_is_staging_site',%s)", "true"
324
+ )
325
+ );
326
+ }
327
+
328
+ // All good
329
+ if( $result ) {
330
+ return true;
331
+ }
332
+
333
+ $this->log( "Preparing Data Step2: Failed to update wpstg_is_staging_site in {$this->prefix}options {$this->db->last_error}", Logger::TYPE_ERROR );
334
+ return false;
335
+ }
336
+
337
+ /**
338
+ * Update rewrite_rules
339
+ * @return bool
340
+ */
341
+ protected function step3() {
342
+
343
+ $this->log( "Preparing Data Step3: Updating rewrite_rules in {$this->prefix}options {$this->db->last_error}" );
344
+
345
+ // Skip - Table does not exist
346
+ if( false === $this->isTable( $this->prefix . 'options' ) ) {
347
+ return true;
348
+ }
349
+
350
+ // Skip - Table is not selected or updated
351
+ if( !in_array( $this->prefix . 'options', $this->tables ) ) {
352
+ $this->log( "Preparing Data Step3: Skipping" );
353
+ return true;
354
+ }
355
+
356
+ $result = $this->db->query(
357
+ $this->db->prepare(
358
+ "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'rewrite_rules'", ' '
359
+ )
360
+ );
361
+
362
+ // All good
363
+ if( $result ) {
364
+ return true;
365
+ }
366
+
367
+ $this->log( "Preparing Data Step3: Failed to update rewrite_rules in {$this->prefix}options {$this->db->last_error}", Logger::TYPE_ERROR );
368
+ return true;
369
+ }
370
+
371
+ /**
372
+ * Update Table Prefix in wp_usermeta
373
+ * @return bool
374
+ */
375
+ protected function step4() {
376
+ $this->log( "Preparing Data Step4: Updating db prefix in {$this->prefix}usermeta. " );
377
+
378
+ // Skip - Table does not exist
379
+ if( false === $this->isTable( $this->prefix . 'usermeta' ) ) {
380
+ return true;
381
+ }
382
+
383
+ // Skip - Table is not selected or updated
384
+ if( !in_array( $this->prefix . 'usermeta', $this->tables ) ) {
385
+ $this->log( "Preparing Data Step4: Skipping" );
386
+ return true;
387
+ }
388
+
389
+ // Skip, prefixes are identical. No change needed
390
+ // if( $this->db->prefix === $this->prefix ) {
391
+ // $this->log( "Preparing Data Step4: Skipping" );
392
+ // return true;
393
+ // }
394
+
395
+ $update = $this->db->query(
396
+ $this->db->prepare(
397
+ "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 . "_%"
398
+ )
399
+ );
400
+
401
+ if( !$update ) {
402
+ $this->log( "Preparing Data Step4a: Skip updating {$this->prefix}usermeta meta_key database base_prefix; {$this->db->last_error}", Logger::TYPE_INFO );
403
+ //return true;
404
+ }
405
+
406
+ $update = $this->db->query(
407
+ $this->db->prepare(
408
+ "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 . "_%"
409
+ )
410
+ );
411
+
412
+ if( !$update ) {
413
+ $this->log( "Preparing Data Step4: Skip updating {$this->prefix}usermeta meta_key database table prefixes; {$this->db->last_error}", Logger::TYPE_INFO );
414
+ //$this->returnException( "Data Crunching Step 4: Failed to update {$this->prefix}usermeta meta_key database table prefixes; {$this->db->last_error}" );
415
+ return true;
416
+ }
417
+ return true;
418
+ }
419
+
420
+ /**
421
+ * Update $table_prefix in wp-config.php
422
+ * @return bool
423
+ */
424
+ protected function step5() {
425
+ $path = $this->options->destinationDir . "wp-config.php";
426
+
427
+ $this->log( "Preparing Data Step5: Updating table_prefix in {$path} to " . $this->prefix );
428
+ if( false === ($content = file_get_contents( $path )) ) {
429
+ $this->log( "Preparing Data Step5: Failed to update table_prefix in {$path}. Can't read contents", Logger::TYPE_ERROR );
430
+ return false;
431
+ }
432
+
433
+ // Replace table prefix
434
+ $content = str_replace( '$table_prefix', '$table_prefix = \'' . $this->prefix . '\';//', $content );
435
+
436
+ // Replace URLs
437
+ $content = str_replace( $this->multisiteHomeDomain, $this->getStagingSiteUrl(), $content );
438
+
439
+ if( false === @file_put_contents( $path, $content ) ) {
440
+ $this->log( "Preparing Data Step5: Failed to update $table_prefix in {$path} to " . $this->prefix . ". Can't save contents", Logger::TYPE_ERROR );
441
+ return false;
442
+ }
443
+
444
+ return true;
445
+ }
446
+
447
+ /**
448
+ * Reset index.php to original file
449
+ * This is needed if live site is located in subfolder
450
+ * Check first if main wordpress is used in subfolder and index.php in parent directory
451
+ * @see: https://codex.wordpress.org/Giving_WordPress_Its_Own_Directory
452
+ * @return bool
453
+ */
454
+ protected function step6() {
455
+
456
+ if( !$this->isSubDir() ) {
457
+ $this->debugLog( "Preparing Data Step6: WP installation is not in a subdirectory! All good, skipping this step" );
458
+ return true;
459
+ }
460
+
461
+ $path = $this->options->destinationDir . "index.php";
462
+
463
+ if( false === ($content = file_get_contents( $path )) ) {
464
+ $this->log( "Preparing Data Step6: Failed to reset {$path} for sub directory; can't read contents", Logger::TYPE_ERROR );
465
+ return false;
466
+ }
467
+
468
+
469
+ if( !preg_match( "/(require(.*)wp-blog-header.php' \);)/", $content, $matches ) ) {
470
+ $this->log(
471
+ "Preparing Data Step6: Failed to reset index.php for sub directory; wp-blog-header.php is missing", Logger::TYPE_ERROR
472
+ );
473
+ return false;
474
+ }
475
+ $this->log( "Preparing Data: WP installation is in a subdirectory. Progressing..." );
476
+
477
+ $pattern = "/require(.*) dirname(.*) __FILE__ (.*) \. '(.*)wp-blog-header.php'(.*);/";
478
+
479
+ $replace = "require( dirname( __FILE__ ) . '/wp-blog-header.php' ); // " . $matches[0];
480
+ $replace.= " // Changed by WP-Staging";
481
+
482
+
483
+
484
+ if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
485
+ $this->log( "Preparing Data: Failed to reset index.php for sub directory; replacement failed", Logger::TYPE_ERROR );
486
+ return false;
487
+ }
488
+
489
+ if( false === @file_put_contents( $path, $content ) ) {
490
+ $this->log( "Preparing Data: Failed to reset index.php for sub directory; can't save contents", Logger::TYPE_ERROR );
491
+ return false;
492
+ }
493
+ $this->Log( "Preparing Data: Finished Step 6 successfully" );
494
+ return true;
495
+ }
496
+
497
+ /**
498
+ * Update wpstg_rmpermalinks_executed
499
+ * @return bool
500
+ */
501
+ protected function step7() {
502
+
503
+ $this->log( "Preparing Data Step7: Updating wpstg_rmpermalinks_executed in {$this->prefix}options {$this->db->last_error}" );
504
+
505
+ // Skip - Table does not exist
506
+ if( false === $this->isTable( $this->prefix . 'options' ) ) {
507
+ return true;
508
+ }
509
+
510
+ // Skip - Table is not selected or updated
511
+ if( !in_array( $this->prefix . 'options', $this->tables ) ) {
512
+ $this->log( "Preparing Data Step7: Skipping" );
513
+ return true;
514
+ }
515
+
516
+ $result = $this->db->query(
517
+ $this->db->prepare(
518
+ "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'wpstg_rmpermalinks_executed'", ' '
519
+ )
520
+ );
521
+
522
+ // All good
523
+ if( $result ) {
524
+ $this->Log( "Preparing Data Step7: Finished Step 7 successfully" );
525
+ return true;
526
+ }
527
+
528
+ $this->log( "Failed to update wpstg_rmpermalinks_executed in {$this->prefix}options {$this->db->last_error}", Logger::TYPE_WARNING );
529
+ return true;
530
+ }
531
+
532
+ /**
533
+ * Update permalink_structure
534
+ * @return bool
535
+ */
536
+ protected function step8() {
537
+
538
+ $this->log( "Preparing Data Step8: Updating permalink_structure in {$this->prefix}options {$this->db->last_error}" );
539
+
540
+ // Skip - Table does not exist
541
+ if( false === $this->isTable( $this->prefix . 'options' ) ) {
542
+ return true;
543
+ }
544
+
545
+ // Skip - Table is not selected or updated
546
+ if( !in_array( $this->prefix . 'options', $this->tables ) ) {
547
+ $this->log( "Preparing Data Step8: Skipping" );
548
+ return true;
549
+ }
550
+
551
+ $result = $this->db->query(
552
+ $this->db->prepare(
553
+ "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'permalink_structure'", ' '
554
+ )
555
+ );
556
+
557
+ // All good
558
+ if( $result ) {
559
+ $this->Log( "Preparing Data Step8: Finished Step 8 successfully" );
560
+ return true;
561
+ }
562
+
563
+ $this->log( "Failed to update permalink_structure in {$this->prefix}options {$this->db->last_error}", Logger::TYPE_ERROR );
564
+ return true;
565
+ }
566
+
567
+ /**
568
+ * Update blog_public option to not allow staging site to be indexed by search engines
569
+ * @return bool
570
+ */
571
+ protected function step9() {
572
+
573
+ $this->log( "Preparing Data Step9: Set staging site to noindex" );
574
+
575
+ if( false === $this->isTable( $this->prefix . 'options' ) ) {
576
+ return true;
577
+ }
578
+
579
+ // Skip - Table is not selected or updated
580
+ if( !in_array( $this->prefix . 'options', $this->tables ) ) {
581
+ $this->log( "Preparing Data Step9: Skipping" );
582
+ return true;
583
+ }
584
+
585
+ $result = $this->db->query(
586
+ $this->db->prepare(
587
+ "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'blog_public'", '0'
588
+ )
589
+ );
590
+
591
+ // All good
592
+ if( $result ) {
593
+ $this->Log( "Preparing Data Step9: Finished Step 9 successfully" );
594
+ return true;
595
+ }
596
+
597
+ $this->log( "Can not update staging site to noindex. Possible already done!", Logger::TYPE_WARNING );
598
+ return true;
599
+ }
600
+
601
+ /**
602
+ * Update WP_HOME in wp-config.php
603
+ * @return bool
604
+ */
605
+ protected function step10() {
606
+ $path = $this->options->destinationDir . "wp-config.php";
607
+
608
+ $this->log( "Preparing Data Step10: Updating WP_HOME in wp-config.php to " . $this->getStagingSiteUrl() );
609
+
610
+ if( false === ($content = file_get_contents( $path )) ) {
611
+ $this->log( "Preparing Data Step10: Failed to update WP_HOME in wp-config.php. Can't read wp-config.php", Logger::TYPE_ERROR );
612
+ return false;
613
+ }
614
+
615
+
616
+ // Get WP_HOME from wp-config.php
617
+ preg_match( "/define\s*\(\s*'WP_HOME'\s*,\s*(.*)\s*\);/", $content, $matches );
618
+
619
+ if( !empty( $matches[1] ) ) {
620
+ $matches[1];
621
+
622
+ $pattern = "/define\s*\(\s*'WP_HOME'\s*,\s*(.*)\s*\);/";
623
+
624
+ $replace = "define('WP_HOME','" . $this->getStagingSiteUrl() . "'); // " . $matches[1];
625
+ $replace.= " // Changed by WP-Staging";
626
+
627
+ if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
628
+ $this->log( "Preparing Data: Failed to update WP_HOME", Logger::TYPE_ERROR );
629
+ return false;
630
+ }
631
+ } else {
632
+ $this->log( "Preparing Data Step10: WP_HOME not defined in wp-config.php. Skipping this step." );
633
+ }
634
+
635
+ if( false === @file_put_contents( $path, $content ) ) {
636
+ $this->log( "Preparing Data Step10: Failed to update WP_HOME. Can't save contents", Logger::TYPE_ERROR );
637
+ return false;
638
+ }
639
+ $this->Log( "Preparing Data: Finished Step 10 successfully" );
640
+ return true;
641
+ }
642
+
643
+ /**
644
+ * Update WP_SITEURL in wp-config.php
645
+ * @return bool
646
+ */
647
+ protected function step11() {
648
+ $path = $this->options->destinationDir . "wp-config.php";
649
+
650
+ $this->log( "Preparing Data Step11: Updating WP_SITEURL in wp-config.php to " . $this->getStagingSiteUrl() );
651
+
652
+ if( false === ($content = file_get_contents( $path )) ) {
653
+ $this->log( "Preparing Data Step11: Failed to update WP_SITEURL in wp-config.php. Can't read wp-config.php", Logger::TYPE_ERROR );
654
+ return false;
655
+ }
656
+
657
+
658
+ // Get WP_SITEURL from wp-config.php
659
+ preg_match( "/define\s*\(\s*'WP_SITEURL'\s*,\s*(.*)\s*\);/", $content, $matches );
660
+
661
+ if( !empty( $matches[1] ) ) {
662
+ $matches[1];
663
+
664
+ $pattern = "/define\s*\(\s*'WP_SITEURL'\s*,\s*(.*)\s*\);/";
665
+
666
+ $replace = "define('WP_SITEURL','" . $this->getStagingSiteUrl() . "'); // " . $matches[1];
667
+ $replace.= " // Changed by WP-Staging";
668
+
669
+ if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
670
+ $this->log( "Preparing Data Step11: Failed to update WP_SITEURL", Logger::TYPE_ERROR );
671
+ return false;
672
+ }
673
+ } else {
674
+ $this->log( "Preparing Data Step11: WP_SITEURL not defined in wp-config.php. Skipping this step." );
675
+ }
676
+
677
+
678
+ if( false === @file_put_contents( $path, $content ) ) {
679
+ $this->log( "Preparing Data Step11: Failed to update WP_SITEURL. Can't save contents", Logger::TYPE_ERROR );
680
+ return false;
681
+ }
682
+ $this->Log( "Preparing Data: Finished Step 11 successfully" );
683
+ return true;
684
+ }
685
+
686
+ /**
687
+ * Update WP_ALLOW_MULTISITE constant in wp-config.php
688
+ * @return bool
689
+ */
690
+ protected function step12() {
691
+ $path = $this->options->destinationDir . "wp-config.php";
692
+
693
+ $this->log( "Preparing Data Step12: Updating WP_ALLOW_MULTISITE in wp-config.php to false" );
694
+
695
+ if( false === ($content = file_get_contents( $path )) ) {
696
+ $this->log( "Preparing Data Step12: Failed to update WP_ALLOW_MULTISITE in wp-config.php. Can't read wp-config.php", Logger::TYPE_ERROR );
697
+ return false;
698
+ }
699
+
700
+
701
+ // Get WP_SITEURL from wp-config.php
702
+ preg_match( "/define\s*\(\s*'WP_ALLOW_MULTISITE'\s*,\s*(.*)\s*\);/", $content, $matches );
703
+
704
+ if( !empty( $matches[1] ) ) {
705
+ $matches[1];
706
+
707
+ $pattern = "/define\s*\(\s*'WP_ALLOW_MULTISITE'\s*,\s*(.*)\s*\);/";
708
+
709
+ $replace = "define('WP_ALLOW_MULTISITE',false); // " . $matches[1];
710
+ $replace.= " // Changed by WP-Staging";
711
+
712
+ if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
713
+ $this->log( "Preparing Data Step12: Failed to update WP_ALLOW_MULTISITE", Logger::TYPE_ERROR );
714
+ return false;
715
+ }
716
+ } else {
717
+ $this->log( "Preparing Data Step12: WP_ALLOW_MULTISITE not defined in wp-config.php. Skipping this step." );
718
+ }
719
+
720
+
721
+ if( false === @file_put_contents( $path, $content ) ) {
722
+ $this->log( "Preparing Data Step12: Failed to update WP_ALLOW_MULTISITE. Can't save contents", Logger::TYPE_ERROR );
723
+ return false;
724
+ }
725
+ $this->Log( "Preparing Data: Finished Step 12 successfully" );
726
+ return true;
727
+ }
728
+
729
+ /**
730
+ * Update MULTISITE constant in wp-config.php
731
+ * @return bool
732
+ */
733
+ protected function step13() {
734
+ $path = $this->options->destinationDir . "wp-config.php";
735
+
736
+ $this->log( "Preparing Data Step13: Updating MULTISITE in wp-config.php to false" );
737
+
738
+ if( false === ($content = file_get_contents( $path )) ) {
739
+ $this->log( "Preparing Data Step13: Failed to update MULTISITE in wp-config.php. Can't read wp-config.php", Logger::TYPE_ERROR );
740
+ return false;
741
+ }
742
+
743
+
744
+ // Get WP_SITEURL from wp-config.php
745
+ preg_match( "/define\s*\(\s*'MULTISITE'\s*,\s*(.*)\s*\);/", $content, $matches );
746
+
747
+ if( !empty( $matches[1] ) ) {
748
+ $matches[1];
749
+
750
+ $pattern = "/define\s*\(\s*'MULTISITE'\s*,\s*(.*)\s*\);/";
751
+
752
+ $replace = "define('MULTISITE',false); // " . $matches[1];
753
+ $replace.= " // Changed by WP-Staging";
754
+
755
+ if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
756
+ $this->log( "Preparing Data Step13: Failed to update MULTISITE", Logger::TYPE_ERROR );
757
+ return false;
758
+ }
759
+ } else {
760
+ $this->log( "Preparing Data Step13: MULTISITE not defined in wp-config.php. Skipping this step." );
761
+ }
762
+
763
+
764
+ if( false === @file_put_contents( $path, $content ) ) {
765
+ $this->log( "Preparing Data Step13: Failed to update MULTISITE. Can't save contents", Logger::TYPE_ERROR );
766
+ return false;
767
+ }
768
+ $this->Log( "Preparing Data: Finished Step 13 successfully" );
769
+ return true;
770
+ }
771
+
772
+ /**
773
+ * Get active_sitewide_plugins from wp_sitemeta and active_plugins from subsite
774
+ * Merge both arrays and copy them to the staging site into active_plugins
775
+ */
776
+ protected function step14() {
777
+
778
+
779
+ $this->log( "Data Crunching Step 14: Updating active_plugins" );
780
+
781
+ if( false === $this->isTable( $this->prefix . 'options' ) ) {
782
+ $this->log( 'Data Crunching Step 14: Fatal Error ' . $this->prefix . 'options does not exist' );
783
+ $this->returnException( 'Data Crunching Step 14: Fatal Error ' . $this->prefix . 'options does not exist' );
784
+ return false;
785
+ }
786
+
787
+ // Skip - Table is not selected or updated
788
+ if( !in_array( $this->prefix . 'options', $this->tables ) ) {
789
+ $this->log( "Preparing Data Step14: Skipping" );
790
+ return true;
791
+ }
792
+
793
+ // Get active_plugins value from sub site options table
794
+ $active_plugins = $this->productionDb->get_var( "SELECT option_value FROM {$this->productionDb->prefix}options WHERE option_name = 'active_plugins' " );
795
+
796
+ if( !$active_plugins ) {
797
+ $this->log( "Data Crunching Step 14: Option active_plugins are empty " );
798
+ $active_plugins = array();
799
+ }
800
+ // Get active_sitewide_plugins value from main multisite wp_sitemeta table
801
+ $active_sitewide_plugins = $this->productionDb->get_var( "SELECT meta_value FROM {$this->productionDb->base_prefix}sitemeta WHERE meta_key = 'active_sitewide_plugins' " );
802
+
803
+ if( !$active_sitewide_plugins ) {
804
+ $this->log( "Data Crunching Step 14: Options {$this->productionDb->base_prefix}active_sitewide_plugins is empty " );
805
+ $active_sitewide_plugins = array();
806
+ }
807
+
808
+ $active_sitewide_plugins = unserialize( $active_sitewide_plugins );
809
+ $active_plugins = unserialize( $active_plugins );
810
+
811
+ $all_plugins = array_merge( $active_plugins, array_keys( $active_sitewide_plugins ) );
812
+
813
+ sort( $all_plugins );
814
+
815
+
816
+ // Update active_plugins
817
+ $update = $this->db->query(
818
+ "UPDATE {$this->prefix}options SET option_value = '" . serialize( $all_plugins ) . "' WHERE option_name = 'active_plugins'"
819
+ );
820
+
821
+ if( false === $update ) {
822
+ $this->log( "Data Crunching Step 14: Can not update option active_plugins in {$this->prefix}options", Logger::TYPE_WARNING );
823
+ return false;
824
+ }
825
+
826
+ $this->log( "Data Crunching Step 14: Successfull!" );
827
+ return true;
828
+ }
829
+
830
+ /**
831
+ * Update Table Prefix in wp_options
832
+ * @return bool
833
+ */
834
+ protected function step15() {
835
+ $this->log( "Preparing Data Step15: Updating db prefix in {$this->prefix}options." );
836
+
837
+ // Skip - Table does not exist
838
+ if( false === $this->isTable( $this->prefix . 'options' ) ) {
839
+ return true;
840
+ }
841
+
842
+ // Skip - Table is not selected or updated
843
+ if( !in_array( $this->prefix . 'options', $this->tables ) ) {
844
+ $this->log( "Preparing Data Step4: Skipping" );
845
+ return true;
846
+ }
847
+
848
+ // Skip, prefixes are identical. No change needed
849
+ // if( $this->productionDb->prefix === $this->prefix ) {
850
+ // $this->log( "Preparing Data Step4: Skipping" );
851
+ // return true;
852
+ // }
853
+
854
+ $this->log( "Updating db option_names in {$this->prefix}options. " );
855
+
856
+ // Filter the rows below. Do not update them!
857
+ $filters = array(
858
+ 'wp_mail_smtp',
859
+ 'wp_mail_smtp_version',
860
+ 'wp_mail_smtp_debug',
861
+ );
862
+
863
+ $filters = apply_filters( 'wpstg_data_excl_rows', $filters );
864
+
865
+ $where = "";
866
+ foreach ( $filters as $filter ) {
867
+ $where .= " AND option_name <> '" . $filter . "'";
868
+ }
869
+
870
+ $updateOptions = $this->db->query(
871
+ $this->db->prepare(
872
+ "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 . "_%"
873
+ )
874
+ );
875
+
876
+ if( !$updateOptions ) {
877
+ $this->log( "Preparing Data Step15: Skip updating db option_names in {$this->prefix}options. Error: {$this->db->last_error}", Logger::TYPE_WARNING );
878
+ //$this->returnException( "Data Crunching Step 15: Failed to update db option_names in {$this->prefix}options. Error: {$this->db->last_error}" );
879
+ return true;
880
+ }
881
+
882
+
883
+ return true;
884
+ }
885
+
886
+ /**
887
+ * Change upload_path in wp_options (if it is defined)
888
+ * @return bool
889
+ */
890
+ protected function step16() {
891
+ $this->log( "Preparing Data Step16: Updating upload_path {$this->prefix}options." );
892
+
893
+ // Skip - Table does not exist
894
+ if( false === $this->isTable( $this->prefix . 'options' ) ) {
895
+ return true;
896
+ }
897
+
898
+ $newUploadPath = $this->getNewUploadPath();
899
+
900
+ if( false === $newUploadPath ) {
901
+ $this->log( "Preparing Data Step16: Skipping" );
902
+ return true;
903
+ }
904
+
905
+ // Skip - Table is not selected or updated
906
+ if( !in_array( $this->prefix . 'options', $this->tables ) ) {
907
+ $this->log( "Preparing Data Step16: Skipping" );
908
+ return true;
909
+ }
910
+
911
+ $error = isset( $this->db->last_error ) ? 'Last error: ' . $this->db->last_error : '';
912
+
913
+ $this->log( "Updating upload_path in {$this->prefix}options. {$error}" );
914
+
915
+ $updateOptions = $this->db->query(
916
+ $this->db->prepare(
917
+ "UPDATE {$this->prefix}options SET option_value = %s WHERE option_name = 'upload_path'", $newUploadPath
918
+ )
919
+ );
920
+
921
+ if( !$updateOptions ) {
922
+ $this->log( "Preparing Data Step16: Failed to update upload_path in {$this->prefix}options. {$error}", Logger::TYPE_ERROR );
923
+ return true;
924
+ }
925
+ $this->Log( "Preparing Data: Finished Step 16 successfully" );
926
+ return true;
927
+ }
928
+
929
+ /**
930
+ * Update WP_CACHE in wp-config.php
931
+ * @return bool
932
+ */
933
+ protected function step17() {
934
+ $path = $this->options->destinationDir . "wp-config.php";
935
+
936
+ $this->log( "Preparing Data Step17: Set WP_CACHE in wp-config.php to false" );
937
+
938
+ if( false === ($content = file_get_contents( $path )) ) {
939
+ $this->log( "Preparing Data Step17: Failed to update WP_CACHE in wp-config.php. Can't read wp-config.php", Logger::TYPE_ERROR );
940
+ return false;
941
+ }
942
+
943
+
944
+ // Get WP_CACHE from wp-config.php
945
+ preg_match( "/define\s*\(\s*'WP_CACHE'\s*,\s*(.*)\s*\);/", $content, $matches );
946
+
947
+ if( !empty( $matches[1] ) ) {
948
+ $matches[1];
949
+
950
+ $pattern = "/define\s*\(\s*'WP_CACHE'\s*,\s*(.*)\s*\);/";
951
+
952
+ $replace = "define('WP_CACHE',false); // " . $matches[1];
953
+ $replace.= " // Changed by WP-Staging";
954
+
955
+ if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
956
+ $this->log( "Preparing Data: Failed to change WP_CACHE", Logger::TYPE_ERROR );
957
+ return false;
958
+ }
959
+ } else {
960
+ $this->log( "Preparing Data Step17: WP_CACHE not defined in wp-config.php. Skipping this step." );
961
+ }
962
+
963
+ if( false === @file_put_contents( $path, $content ) ) {
964
+ $this->log( "Preparing Data Step17: Failed to update WP_CACHE. Can't save contents", Logger::TYPE_ERROR );
965
+ return false;
966
+ }
967
+ $this->Log( "Preparing Data: Finished Step 17 successfully" );
968
+ return true;
969
+ }
970
+
971
+ /**
972
+ * Update database credentials in wp-config.php
973
+ * @return bool
974
+ */
975
+ protected function step18() {
976
+ $path = $this->options->destinationDir . "wp-config.php";
977
+
978
+ $this->log( "Preparing Data Step18: Change database credentials in wp-config.php" );
979
+
980
+ if( false === ($content = file_get_contents( $path )) ) {
981
+ $this->log( "Preparing Data Step18: Failed to update database credentials in wp-config.php. Can't read wp-config.php", Logger::TYPE_ERROR );
982
+ return false;
983
+ }
984
+
985
+
986
+ // Get DB_NAME from wp-config.php
987
+ preg_match( "/define\s*\(\s*'DB_NAME'\s*,\s*(.*)\s*\);/", $content, $matches );
988
+
989
+ if( !empty( $matches[1] ) ) {
990
+ $matches[1];
991
+
992
+ $pattern = "/define\s*\(\s*'DB_NAME'\s*,\s*(.*)\s*\);/";
993
+
994
+ $replace = "define('DB_NAME','{$this->options->databaseDatabase}'); // " . $matches[1];
995
+ $replace.= " // Changed by WP-Staging";
996
+
997
+ if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
998
+ $this->log( "Preparing Data: Failed to change DB_NAME", Logger::TYPE_ERROR );
999
+ return false;
1000
+ }
1001
+ } else {
1002
+ $this->log( "Preparing Data Step18: DB_NAME not defined in wp-config.php. Skipping this step." );
1003
+ }
1004
+ // Get DB_USER from wp-config.php
1005
+ preg_match( "/define\s*\(\s*'DB_USER'\s*,\s*(.*)\s*\);/", $content, $matches );
1006
+
1007
+ if( !empty( $matches[1] ) ) {
1008
+ $matches[1];
1009
+
1010
+ $pattern = "/define\s*\(\s*'DB_USER'\s*,\s*(.*)\s*\);/";
1011
+
1012
+ $replace = "define('DB_USER','{$this->options->databaseUser}'); // " . $matches[1];
1013
+ $replace.= " // Changed by WP-Staging";
1014
+
1015
+ if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
1016
+ $this->log( "Preparing Data: Failed to change DB_USER", Logger::TYPE_ERROR );
1017
+ return false;
1018
+ }
1019
+ } else {
1020
+ $this->log( "Preparing Data Step18: DB_USER not defined in wp-config.php. Skipping this step." );
1021
+ }
1022
+ // Get DB_PASSWORD from wp-config.php
1023
+ preg_match( "/define\s*\(\s*'DB_PASSWORD'\s*,\s*(.*)\s*\);/", $content, $matches );
1024
+
1025
+ if( !empty( $matches[1] ) ) {
1026
+ $matches[1];
1027
+
1028
+ $pattern = "/define\s*\(\s*'DB_PASSWORD'\s*,\s*(.*)\s*\);/";
1029
+
1030
+ $replace = "define('DB_PASSWORD','{$this->options->databasePassword}'); // " . $matches[1];
1031
+ $replace.= " // Changed by WP-Staging";
1032
+
1033
+ if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
1034
+ $this->log( "Preparing Data: Failed to change DB_PASSWORD", Logger::TYPE_ERROR );
1035
+ return false;
1036
+ }
1037
+ } else {
1038
+ $this->log( "Preparing Data Step18: DB_PASSWORD not defined in wp-config.php. Skipping this step." );
1039
+ }
1040
+ // Get DB_HOST from wp-config.php
1041
+ preg_match( "/define\s*\(\s*'DB_HOST'\s*,\s*(.*)\s*\);/", $content, $matches );
1042
+
1043
+ if( !empty( $matches[1] ) ) {
1044
+ $matches[1];
1045
+
1046
+ $pattern = "/define\s*\(\s*'DB_HOST'\s*,\s*(.*)\s*\);/";
1047
+
1048
+ $replace = "define('DB_HOST','{$this->options->databaseServer}'); // " . $matches[1];
1049
+ $replace.= " // Changed by WP-Staging";
1050
+
1051
+ if( null === ($content = preg_replace( array($pattern), $replace, $content )) ) {
1052
+ $this->log( "Preparing Data: Failed to change DB_HOST", Logger::TYPE_ERROR );
1053
+ return false;
1054
+ }
1055
+ } else {
1056
+ $this->log( "Preparing Data Step18: DB_HOST not defined in wp-config.php. Skipping this step." );
1057
+ }
1058
+
1059
+
1060
+ if( false === @file_put_contents( $path, $content ) ) {
1061
+ $this->log( "Preparing Data Step18: Failed to update database credentials in wp-config.php. Can't save contents", Logger::TYPE_ERROR );
1062
+ return false;
1063
+ }
1064
+ $this->Log( "Preparing Data: Finished Step 18 successfully" );
1065
+ return true;
1066
+ }
1067
+
1068
+ /**
1069
+ * Get upload path
1070
+ * @return boolean|string
1071
+ */
1072
+ protected function getNewUploadPath() {
1073
+ $uploadPath = get_option( 'upload_path' );
1074
+
1075
+ if( !$uploadPath ) {
1076
+ return false;
1077
+ }
1078
+
1079
+ $customSlug = str_replace( \WPStaging\WPStaging::getWPpath(), '', $uploadPath );
1080
+
1081
+ $newUploadPath = \WPStaging\WPStaging::getWPpath() . $this->options->cloneDirectoryName . DIRECTORY_SEPARATOR . $customSlug;
1082
+
1083
+ return $newUploadPath;
1084
+ }
1085
+
1086
+ /**
1087
+ * Return URL to staging site
1088
+ * @return string
1089
+ */
1090
+ protected function getStagingSiteUrl() {
1091
+
1092
+ if( !empty( $this->options->cloneHostname ) ) {
1093
+ return $this->options->cloneHostname;
1094
+ }
1095
+
1096
+ if( $this->isSubDir() ) {
1097
+ return trailingslashit( $this->multisiteHomeDomain) . trailingslashit( $this->getSubDir() ) . $this->options->cloneDirectoryName;
1098
+ }
1099
+
1100
+ return trailingslashit( $this->multisiteHomeDomain) . $this->options->cloneDirectoryName;
1101
+ }
1102
+
1103
+ /**
1104
+ * Check if WP is installed in subdir
1105
+ * @return boolean
1106
+ */
1107
+ protected function isSubDir() {
1108
+ // Compare names without scheme to bypass cases where siteurl and home have different schemes http / https
1109
+ // This is happening much more often than you would expect
1110
+ $siteurl = preg_replace( '#^https?://#', '', rtrim( get_option( 'siteurl' ), '/' ) );
1111
+ $home = preg_replace( '#^https?://#', '', rtrim( get_option( 'home' ), '/' ) );
1112
+
1113
+ if( $home !== $siteurl ) {
1114
+ return true;
1115
+ }
1116
+ return false;
1117
+ }
1118
+
1119
+ /**
1120
+ * Get the install sub directory if WP is installed in sub directory
1121
+ * @return string
1122
+ */
1123
+ protected function getSubDir() {
1124
+ $home = get_option( 'home' );
1125
+ $siteurl = get_option( 'siteurl' );
1126
+
1127
+ if( empty( $home ) || empty( $siteurl ) ) {
1128
+ return '';
1129
+ }
1130
+
1131
+ $dir = str_replace( $home, '', $siteurl );
1132
+ return str_replace( '/', '', $dir );
1133
+ }
1134
+
1135
+ }
apps/Backend/Modules/Jobs/Multisite/Database.php CHANGED
@@ -1,362 +1,362 @@
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
- if( isset( $this->options->mainJob ) && $this->options->mainJob !== 'updating' && is_dir( $path ) ) {
63
- $this->returnException( " Can not continue! Change the name of the clone or delete existing folder. Then try again. Folder already exists: " . $path );
64
- }
65
- return false;
66
- }
67
-
68
- /**
69
- * Calculate Total Steps in This Job and Assign It to $this->options->totalSteps
70
- * @return void
71
- */
72
- protected function calculateTotalSteps() {
73
- $this->options->totalSteps = ($this->total === 0) ? 1 : $this->total;
74
- }
75
-
76
- /**
77
- * Execute the Current Step
78
- * Returns false when over threshold limits are hit or when the job is done, true otherwise
79
- * @return bool
80
- */
81
- protected function execute() {
82
-
83
-
84
- // Over limits threshold
85
- if( $this->isOverThreshold() ) {
86
- // Prepare response and save current progress
87
- $this->prepareResponse( false, false );
88
- $this->saveOptions();
89
- return false;
90
- }
91
-
92
- // No more steps, finished
93
- if( $this->options->currentStep > $this->total ) {
94
- $this->prepareResponse( true, false );
95
- return false;
96
- }
97
-
98
- // Copy table
99
- if( isset( $this->options->tables[$this->options->currentStep] ) && !$this->copyTable( $this->options->tables[$this->options->currentStep] ) ) {
100
- // Prepare Response
101
- $this->prepareResponse( false, false );
102
-
103
- // Not finished
104
- return true;
105
- }
106
-
107
- // if( isset( $this->options->tables[$this->options->currentStep] ) && $this->db->prefix . 'users' === $this->options->tables[$this->options->currentStep] ) {
108
- // $this->copyWpUsers();
109
- // }
110
- // if( isset( $this->options->tables[$this->options->currentStep] ) && $this->db->prefix . 'usermeta' === $this->options->tables[$this->options->currentStep] ) {
111
- // $this->copyWpUsermeta();
112
- // }
113
- // Prepare Response
114
- $this->prepareResponse();
115
-
116
- // Not finished
117
- return true;
118
- }
119
-
120
- /**
121
- * Get new prefix for the staging site
122
- * @return string
123
- */
124
- private function getStagingPrefix() {
125
- $stagingPrefix = $this->options->prefix;
126
- // Make sure prefix of staging site is NEVER identical to prefix of live site!
127
- if( $stagingPrefix == $this->db->prefix ) {
128
- wp_die( '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' );
129
- }
130
- return $stagingPrefix;
131
- }
132
-
133
- /**
134
- * No worries, SQL queries don't eat from PHP execution time!
135
- * @param string $tableName
136
- * @return bool
137
- */
138
- private function copyTable( $tableName ) {
139
-
140
- $strings = new Strings();
141
- $tableName = is_object( $tableName ) ? $tableName->name : $tableName;
142
- $newTableName = $this->getStagingPrefix() . $strings->str_replace_first( $this->db->prefix, null, $tableName );
143
-
144
- // Get wp_users from main site
145
- if( 'users' === $strings->str_replace_first( $this->db->prefix, null, $tableName ) ) {
146
- $tableName = $this->db->base_prefix . 'users';
147
- }
148
- // Get wp_usermeta from main site
149
- if( 'usermeta' === $strings->str_replace_first( $this->db->prefix, null, $tableName ) ) {
150
- $tableName = $this->db->base_prefix . 'usermeta';
151
- }
152
-
153
- // Drop table if necessary
154
- $this->dropTable( $newTableName );
155
-
156
- // Save current job
157
- $this->setJob( $newTableName );
158
-
159
- // Beginning of the job
160
- if( !$this->startJob( $newTableName, $tableName ) ) {
161
- return true;
162
- }
163
-
164
- // Copy data
165
- $this->copyData( $newTableName, $tableName );
166
-
167
- // Finish the step
168
- return $this->finishStep();
169
- }
170
-
171
- /**
172
- * Copy multisite global user table wp_users to wpstgX_users
173
- * @return bool
174
- */
175
- // private function copyWpUsers() {
176
- //// $strings = new Strings();
177
- //// $tableName = $this->db->base_prefix . 'users';
178
- //// $newTableName = $this->getStagingPrefix() . $strings->str_replace_first( $this->db->base_prefix, null, $tableName );
179
- //
180
- // $tableName = $this->db->base_prefix . 'users';
181
- // $newTableName = $this->getStagingPrefix() . 'users';
182
- //
183
- // $this->log( "DB Copy: Try to create table {$newTableName}" );
184
- //
185
- //
186
- // // Drop table if necessary
187
- // $this->dropTable( $newTableName );
188
- //
189
- // // Save current job
190
- // $this->setJob( $newTableName );
191
- //
192
- // // Beginning of the job
193
- // if( !$this->startJob( $newTableName, $tableName ) ) {
194
- // return true;
195
- // }
196
- //
197
- // // Copy data
198
- // $this->copyData( $newTableName, $tableName );
199
- //
200
- // // Finish the step
201
- // return $this->finishStep();
202
- // }
203
-
204
- /**
205
- * Copy multisite global user table wp_usermeta to wpstgX_users
206
- * @return bool
207
- */
208
- // private function copyWpUsermeta() {
209
- //// $strings = new Strings();
210
- //// $tableName = $this->db->base_prefix . 'usermeta';
211
- //// $newTableName = $this->getStagingPrefix() . $strings->str_replace_first( $this->db->base_prefix, null, $tableName );
212
- // $tableName = $this->db->base_prefix . 'usermeta';
213
- // $newTableName = $this->getStagingPrefix() . 'usermeta';
214
- //
215
- // $this->log( "DB Copy: Try to create table {$newTableName}" );
216
- //
217
- //
218
- // // Drop table if necessary
219
- // $this->dropTable( $newTableName );
220
- //
221
- // // Save current job
222
- // $this->setJob( $newTableName );
223
- //
224
- // // Beginning of the job
225
- // if( !$this->startJob( $newTableName, $tableName ) ) {
226
- // return true;
227
- // }
228
- // // Copy data
229
- // $this->copyData( $newTableName, $tableName );
230
- //
231
- // // Finish the step
232
- // return $this->finishStep();
233
- // }
234
-
235
- /**
236
- * Copy data from old table to new table
237
- * @param string $new
238
- * @param string $old
239
- */
240
- private function copyData( $new, $old ) {
241
- $rows = $this->options->job->start + $this->settings->queryLimit;
242
- $this->log(
243
- "DB Copy: {$old} as {$new} from {$this->options->job->start} to {$rows} records"
244
- );
245
-
246
- $limitation = '';
247
-
248
- if( 0 < ( int ) $this->settings->queryLimit ) {
249
- $limitation = " LIMIT {$this->settings->queryLimit} OFFSET {$this->options->job->start}";
250
- }
251
-
252
- $this->db->query(
253
- "INSERT INTO {$new} SELECT * FROM {$old} {$limitation}"
254
- );
255
-
256
- // Set new offset
257
- $this->options->job->start += $this->settings->queryLimit;
258
- }
259
-
260
- /**
261
- * Set the job
262
- * @param string $table
263
- */
264
- private function setJob( $table ) {
265
- if( isset( $this->options->job->current ) ) {
266
- return;
267
- }
268
-
269
- $this->options->job->current = $table;
270
- $this->options->job->start = 0;
271
- }
272
-
273
- /**
274
- * Start Job
275
- * @param string $new
276
- * @param string $old
277
- * @return bool
278
- */
279
- private function startJob( $new, $old ) {
280
-
281
- $this->options->job->total = 0;
282
-
283
- if( 0 != $this->options->job->start ) {
284
- return true;
285
- }
286
-
287
- // Table does not exists
288
- $result = $this->db->query( "SHOW TABLES LIKE '{$old}'" );
289
- if( !$result || 0 === $result ) {
290
- return true;
291
- }
292
-
293
- $this->log( "DB Copy: Creating table {$new}" );
294
-
295
- $this->db->query( "CREATE TABLE {$new} LIKE {$old}" );
296
-
297
- $this->options->job->total = ( int ) $this->db->get_var( "SELECT COUNT(1) FROM {$old}" );
298
-
299
- if( 0 == $this->options->job->total ) {
300
- $this->finishStep();
301
- return false;
302
- }
303
-
304
- return true;
305
- }
306
-
307
- /**
308
- * Finish the step
309
- */
310
- private function finishStep() {
311
- // This job is not finished yet
312
- if( $this->options->job->total > $this->options->job->start ) {
313
- return false;
314
- }
315
-
316
- // Add it to cloned tables listing
317
- $this->options->clonedTables[] = isset( $this->options->tables[$this->options->currentStep] ) ? $this->options->tables[$this->options->currentStep] : false;
318
-
319
- // Reset job
320
- $this->options->job = new \stdClass();
321
-
322
- return true;
323
- }
324
-
325
- /**
326
- * Drop table if necessary
327
- * @param string $new
328
- */
329
- private function dropTable( $new ) {
330
-
331
- $old = $this->db->get_var( $this->db->prepare( "SHOW TABLES LIKE %s", $new ) );
332
-
333
- if( !$this->shouldDropTable( $new, $old ) ) {
334
- return;
335
- }
336
-
337
- $this->log( "DB Copy: {$new} already exists, dropping it first" );
338
- $this->db->query( "DROP TABLE {$new}" );
339
- }
340
-
341
- /**
342
- * Check if table needs to be dropped
343
- * @param string $new
344
- * @param string $old
345
- * @return bool
346
- */
347
- private function shouldDropTable( $new, $old ) {
348
-
349
-
350
-
351
- if( $old === $new &&
352
- (
353
- !isset( $this->options->job->current ) ||
354
- !isset( $this->options->job->start ) ||
355
- 0 == $this->options->job->start
356
- ) ) {
357
- return true;
358
- }
359
- return false;
360
- }
361
-
362
- }
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
+ if( isset( $this->options->mainJob ) && $this->options->mainJob !== 'updating' && is_dir( $path ) ) {
63
+ $this->returnException( " Can not continue! Change the name of the clone or delete existing folder. Then try again. Folder already exists: " . $path );
64
+ }
65
+ return false;
66
+ }
67
+
68
+ /**
69
+ * Calculate Total Steps in This Job and Assign It to $this->options->totalSteps
70
+ * @return void
71
+ */
72
+ protected function calculateTotalSteps() {
73
+ $this->options->totalSteps = ($this->total === 0) ? 1 : $this->total;
74
+ }
75
+
76
+ /**
77
+ * Execute the Current Step
78
+ * Returns false when over threshold limits are hit or when the job is done, true otherwise
79
+ * @return bool
80
+ */
81
+ protected function execute() {
82
+
83
+
84
+ // Over limits threshold
85
+ if( $this->isOverThreshold() ) {
86
+ // Prepare response and save current progress
87
+ $this->prepareResponse( false, false );
88
+ $this->saveOptions();
89
+ return false;
90
+ }
91
+
92
+ // No more steps, finished
93
+ if( $this->options->currentStep > $this->total ) {
94
+ $this->prepareResponse( true, false );
95
+ return false;
96
+ }
97
+
98
+ // Copy table
99
+ if( isset( $this->options->tables[$this->options->currentStep] ) && !$this->copyTable( $this->options->tables[$this->options->currentStep] ) ) {
100
+ // Prepare Response
101
+ $this->prepareResponse( false, false );
102
+
103
+ // Not finished
104
+ return true;
105
+ }
106
+
107
+ // if( isset( $this->options->tables[$this->options->currentStep] ) && $this->db->prefix . 'users' === $this->options->tables[$this->options->currentStep] ) {
108
+ // $this->copyWpUsers();
109
+ // }
110
+ // if( isset( $this->options->tables[$this->options->currentStep] ) && $this->db->prefix . 'usermeta' === $this->options->tables[$this->options->currentStep] ) {
111
+ // $this->copyWpUsermeta();
112
+ // }
113
+ // Prepare Response
114
+ $this->prepareResponse();
115
+
116
+ // Not finished
117
+ return true;
118
+ }
119
+
120
+ /**
121
+ * Get new prefix for the staging site
122
+ * @return string
123
+ */
124
+ private function getStagingPrefix() {
125
+ $stagingPrefix = $this->options->prefix;
126
+ // Make sure prefix of staging site is NEVER identical to prefix of live site!
127
+ if( $stagingPrefix == $this->db->prefix ) {
128
+ wp_die( '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' );
129
+ }
130
+ return $stagingPrefix;
131
+ }
132
+
133
+ /**
134
+ * No worries, SQL queries don't eat from PHP execution time!
135
+ * @param string $tableName
136
+ * @return bool
137
+ */
138
+ private function copyTable( $tableName ) {
139
+
140
+ $strings = new Strings();
141
+ $tableName = is_object( $tableName ) ? $tableName->name : $tableName;
142
+ $newTableName = $this->getStagingPrefix() . $strings->str_replace_first( $this->db->prefix, null, $tableName );
143
+
144
+ // Get wp_users from main site
145
+ if( 'users' === $strings->str_replace_first( $this->db->prefix, null, $tableName ) ) {
146
+ $tableName = $this->db->base_prefix . 'users';
147
+ }
148
+ // Get wp_usermeta from main site
149
+ if( 'usermeta' === $strings->str_replace_first( $this->db->prefix, null, $tableName ) ) {
150
+ $tableName = $this->db->base_prefix . 'usermeta';
151
+ }
152
+
153
+ // Drop table if necessary
154
+ $this->dropTable( $newTableName );
155
+
156
+ // Save current job
157
+ $this->setJob( $newTableName );
158
+
159
+ // Beginning of the job
160
+ if( !$this->startJob( $newTableName, $tableName ) ) {
161
+ return true;
162
+ }
163
+
164
+ // Copy data
165
+ $this->copyData( $newTableName, $tableName );
166
+
167
+ // Finish the step
168
+ return $this->finishStep();
169
+ }
170
+
171
+ /**
172
+ * Copy multisite global user table wp_users to wpstgX_users
173
+ * @return bool
174
+ */
175
+ // private function copyWpUsers() {
176
+ //// $strings = new Strings();
177
+ //// $tableName = $this->db->base_prefix . 'users';
178
+ //// $newTableName = $this->getStagingPrefix() . $strings->str_replace_first( $this->db->base_prefix, null, $tableName );
179
+ //
180
+ // $tableName = $this->db->base_prefix . 'users';
181
+ // $newTableName = $this->getStagingPrefix() . 'users';
182
+ //
183
+ // $this->log( "DB Copy: Try to create table {$newTableName}" );
184
+ //
185
+ //
186
+ // // Drop table if necessary
187
+ // $this->dropTable( $newTableName );
188
+ //
189
+ // // Save current job
190
+ // $this->setJob( $newTableName );
191
+ //
192
+ // // Beginning of the job
193
+ // if( !$this->startJob( $newTableName, $tableName ) ) {
194
+ // return true;
195
+ // }
196
+ //
197
+ // // Copy data
198
+ // $this->copyData( $newTableName, $tableName );
199
+ //
200
+ // // Finish the step
201
+ // return $this->finishStep();
202
+ // }
203
+
204
+ /**
205
+ * Copy multisite global user table wp_usermeta to wpstgX_users
206
+ * @return bool
207
+ */
208
+ // private function copyWpUsermeta() {
209
+ //// $strings = new Strings();
210
+ //// $tableName = $this->db->base_prefix . 'usermeta';
211
+ //// $newTableName = $this->getStagingPrefix() . $strings->str_replace_first( $this->db->base_prefix, null, $tableName );
212
+ // $tableName = $this->db->base_prefix . 'usermeta';
213
+ // $newTableName = $this->getStagingPrefix() . 'usermeta';
214
+ //
215
+ // $this->log( "DB Copy: Try to create table {$newTableName}" );
216
+ //
217
+ //
218
+ // // Drop table if necessary
219
+ // $this->dropTable( $newTableName );
220
+ //
221
+ // // Save current job
222
+ // $this->setJob( $newTableName );
223
+ //
224
+ // // Beginning of the job
225
+ // if( !$this->startJob( $newTableName, $tableName ) ) {
226
+ // return true;
227
+ // }
228
+ // // Copy data
229
+ // $this->copyData( $newTableName, $tableName );
230
+ //
231
+ // // Finish the step
232
+ // return $this->finishStep();
233
+ // }
234
+
235
+ /**
236
+ * Copy data from old table to new table
237
+ * @param string $new
238
+ * @param string $old
239
+ */
240
+ private function copyData( $new, $old ) {
241
+ $rows = $this->options->job->start + $this->settings->queryLimit;
242
+ $this->log(
243
+ "DB Copy: {$old} as {$new} from {$this->options->job->start} to {$rows} records"
244
+ );
245
+
246
+ $limitation = '';
247
+
248
+ if( 0 < ( int ) $this->settings->queryLimit ) {
249
+ $limitation = " LIMIT {$this->settings->queryLimit} OFFSET {$this->options->job->start}";
250
+ }
251
+
252
+ $this->db->query(
253
+ "INSERT INTO {$new} SELECT * FROM {$old} {$limitation}"
254
+ );
255
+
256
+ // Set new offset
257
+ $this->options->job->start += $this->settings->queryLimit;
258
+ }
259
+
260
+ /**
261
+ * Set the job
262
+ * @param string $table
263
+ */
264
+ private function setJob( $table ) {
265
+ if( isset( $this->options->job->current ) ) {
266
+ return;
267
+ }
268
+
269
+ $this->options->job->current = $table;
270
+ $this->options->job->start = 0;
271
+ }
272
+
273
+ /**
274
+ * Start Job
275
+ * @param string $new
276
+ * @param string $old
277
+ * @return bool
278
+ */
279
+ private function startJob( $new, $old ) {
280
+
281
+ $this->options->job->total = 0;
282
+
283
+ if( 0 != $this->options->job->start ) {
284
+ return true;
285
+ }
286
+
287
+ // Table does not exists
288
+ $result = $this->db->query( "SHOW TABLES LIKE '{$old}'" );
289
+ if( !$result || 0 === $result ) {
290
+ return true;
291
+ }
292
+
293
+ $this->log( "DB Copy: Creating table {$new}" );
294
+
295
+ $this->db->query( "CREATE TABLE {$new} LIKE {$old}" );
296
+
297
+ $this->options->job->total = ( int ) $this->db->get_var( "SELECT COUNT(1) FROM {$old}" );
298
+
299
+ if( 0 == $this->options->job->total ) {
300
+ $this->finishStep();
301
+ return false;
302
+ }
303
+
304
+ return true;
305
+ }
306
+
307
+ /**
308
+ * Finish the step
309
+ */
310
+ private function finishStep() {
311
+ // This job is not finished yet
312
+ if( $this->options->job->total > $this->options->job->start ) {
313
+ return false;
314
+ }
315
+
316
+ // Add it to cloned tables listing
317
+ $this->options->clonedTables[] = isset( $this->options->tables[$this->options->currentStep] ) ? $this->options->tables[$this->options->currentStep] : false;
318
+
319
+ // Reset job
320
+ $this->options->job = new \stdClass();
321
+
322
+ return true;
323
+ }
324
+
325
+ /**
326
+ * Drop table if necessary
327
+ * @param string $new
328
+ */
329
+ private function dropTable( $new ) {
330
+
331
+ $old = $this->db->get_var( $this->db->prepare( "SHOW TABLES LIKE %s", $new ) );
332
+
333
+ if( !$this->shouldDropTable( $new, $old ) ) {
334
+ return;
335
+ }
336
+
337
+ $this->log( "DB Copy: {$new} already exists, dropping it first" );
338
+ $this->db->query( "DROP TABLE {$new}" );
339
+ }
340
+
341
+ /**
342
+ * Check if table needs to be dropped
343
+ * @param string $new
344
+ * @param string $old
345
+ * @return bool
346
+ */
347
+ private function shouldDropTable( $new, $old ) {
348
+
349
+
350
+
351
+ if( $old === $new &&
352
+ (
353
+ !isset( $this->options->job->current ) ||
354
+ !isset( $this->options->job->start ) ||
355
+ 0 == $this->options->job->start
356
+ ) ) {
357
+ return true;
358
+ }
359
+ return false;
360
+ }
361
+
362
+ }
apps/Backend/Modules/Jobs/Multisite/DatabaseExternal.php CHANGED
@@ -1,411 +1,411 @@
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
- * Initialize
38
- */
39
- public function initialize() {
40
- $this->db = WPStaging::getInstance()->get( "wpdb" );
41
- $this->stagingDb = $this->getExternalDBConnection();
42
- $this->getTables();
43
- // Add 2 to total table count because we need to copy two more tables from the main multisite installation wp_users and wp_usermeta
44
- $this->total = count( $this->options->tables ) + 2;
45
- $this->isFatalError();
46
- }
47
-
48
- /**
49
- * Get external db object
50
- * @return mixed db | false
51
- */
52
- private function getExternalDBConnection() {
53
-
54
- $db = new \wpdb( $this->options->databaseUser, $this->options->databasePassword, $this->options->databaseDatabase, $this->options->databaseServer );
55
-
56
- // Can not connect to mysql
57
- if( !empty( $db->error->errors['db_connect_fail']['0'] ) ) {
58
- $this->returnException( "Can not connect to external database {$this->options->databaseDatabase}" );
59
- return false;
60
- }
61
-
62
- // Can not connect to database
63
- $sql = "SHOW DATABASES LIKE '{$this->options->databaseDatabase}';";
64
- $results = $db->query( $sql );
65
- if( empty( $results ) ) {
66
- $this->returnException( "Can not connect to external database {$this->options->databaseDatabase}" );
67
- return false;
68
- }
69
- return $db;
70
- }
71
-
72
- private function getTables() {
73
- // Add wp_users and wp_usermeta to the tables if they do not exist
74
- // because they are not available in MU installation but we need them on the staging site
75
-
76
- if( !in_array( $this->db->prefix . 'users', $this->options->tables ) ) {
77
- array_push( $this->options->tables, $this->db->prefix . 'users' );
78
- $this->saveOptions();
79
- }
80
- if( !in_array( $this->db->prefix . 'usermeta', $this->options->tables ) ) {
81
- array_push( $this->options->tables, $this->db->prefix . 'usermeta' );
82
- $this->saveOptions();
83
- }
84
- }
85
-
86
- /**
87
- * Return fatal error and stops here if subfolder already exists
88
- * and mainJob is not updating the clone
89
- * @return boolean
90
- */
91
- private function isFatalError() {
92
- $path = trailingslashit( get_home_path() ) . $this->options->cloneDirectoryName;
93
- if( isset( $this->options->mainJob ) && $this->options->mainJob !== 'updating' && is_dir( $path ) ) {
94
- $this->returnException( " Can not continue! Change the name of the clone or delete existing folder. Then try again. Folder already exists: " . $path );
95
- }
96
- return false;
97
- }
98
-
99
- /**
100
- * Calculate Total Steps in This Job and Assign It to $this->options->totalSteps
101
- * @return void
102
- */
103
- protected function calculateTotalSteps() {
104
- $this->options->totalSteps = ($this->total === 0) ? 1 : $this->total;
105
- }
106
-
107
- /**
108
- * Execute the Current Step
109
- * Returns false when over threshold limits are hit or when the job is done, true otherwise
110
- * @return bool
111
- */
112
- protected function execute() {
113
-
114
-
115
- // Over limits threshold
116
- if( $this->isOverThreshold() ) {
117
- // Prepare response and save current progress
118
- $this->prepareResponse( false, false );
119
- $this->saveOptions();
120
- return false;
121
- }
122
-
123
- // No more steps, finished
124
- if( $this->options->currentStep > $this->total ) {
125
- $this->prepareResponse( true, false );
126
- return false;
127
- }
128
-
129
- // Copy table
130
- if( isset( $this->options->tables[$this->options->currentStep] ) && !$this->copyTable( $this->options->tables[$this->options->currentStep] ) ) {
131
- // Prepare Response
132
- $this->prepareResponse( false, false );
133
-
134
- // Not finished
135
- return true;
136
- }
137
-
138
- // $this->copyWpUsers();
139
- //
140
- // $this->copyWpUsermeta();
141
- // Prepare Response
142
- $this->prepareResponse();
143
-
144
- // Not finished
145
- return true;
146
- }
147
-
148
- /**
149
- * Get new prefix for the staging site
150
- * @return string
151
- */
152
- private function getStagingPrefix() {
153
-
154
- $this->options->prefix = !empty( $this->options->databasePrefix ) ? $this->options->databasePrefix : $this->db->prefix;
155
-
156
- return $this->options->prefix;
157
- }
158
-
159
- /**
160
- * No worries, SQL queries don't eat from PHP execution time!
161
- * @param string $tableName
162
- * @return bool
163
- */
164
- private function copyTable( $tableName ) {
165
-
166
- $strings = new Strings();
167
- $tableName = is_object( $tableName ) ? $tableName->name : $tableName;
168
- $newTableName = $this->getStagingPrefix() . $strings->str_replace_first( $this->db->prefix, null, $tableName );
169
-
170
- // Get wp_users from main site
171
- if( 'users' === $strings->str_replace_first( $this->db->prefix, null, $tableName ) ) {
172
- $tableName = $this->db->base_prefix . 'users';
173
- }
174
- // Get wp_usermeta from main site
175
- if( 'usermeta' === $strings->str_replace_first( $this->db->prefix, null, $tableName ) ) {
176
- $tableName = $this->db->base_prefix . 'usermeta';
177
- }
178
-
179
- // Drop table if necessary
180
- $this->dropTable( $newTableName );
181
-
182
- // Save current job
183
- $this->setJob( $newTableName );
184
-
185
- // Beginning of the job
186
- if( !$this->startJob( $newTableName, $tableName ) ) {
187
- return true;
188
- }
189
-
190
- // Copy data
191
- $this->copyData( $newTableName, $tableName );
192
-
193
- // Finish the step
194
- return $this->finishStep();
195
- }
196
-
197
- /**
198
- * Copy multisite global user table wp_users to wpstgX_users
199
- * @return bool
200
- */
201
- // private function copyWpUsers() {
202
- // $strings = new Strings();
203
- // $tableName = $this->db->base_prefix . 'users';
204
- // $newTableName = $this->getStagingPrefix() . $strings->str_replace_first( $this->db->base_prefix, null, $tableName );
205
- //
206
- // // Drop table if necessary
207
- // $this->dropTable( $newTableName );
208
- //
209
- // // Save current job
210
- // $this->setJob( $newTableName );
211
- //
212
- // // Beginning of the job
213
- // if( !$this->startJob( $newTableName, $tableName ) ) {
214
- // return true;
215
- // }
216
- //
217
- // // Copy data
218
- // $this->copyData( $newTableName, $tableName );
219
- //
220
- // // Finish the step
221
- // return $this->finishStep();
222
- // }
223
-
224
- /**
225
- * Copy multisite global user table wp_usermeta to wpstgX_users
226
- * @return bool
227
- */
228
- // private function copyWpUsermeta() {
229
- // $strings = new Strings();
230
- // $tableName = $this->db->base_prefix . 'usermeta';
231
- // $newTableName = $this->getStagingPrefix() . $strings->str_replace_first( $this->db->base_prefix, null, $tableName );
232
- //
233
- // // Drop table if necessary
234
- // $this->dropTable( $newTableName );
235
- //
236
- // // Save current job
237
- // $this->setJob( $newTableName );
238
- //
239
- // // Beginning of the job
240
- // if( !$this->startJob( $newTableName, $tableName ) ) {
241
- // return true;
242
- // }
243
- // // Copy data
244
- // $this->copyData( $newTableName, $tableName );
245
- //
246
- // // Finish the step
247
- // return $this->finishStep();
248
- // }
249
-
250
- /**
251
- * Copy data from old table to new table
252
- * @param string $new
253
- * @param string $old
254
- */
255
- private function copyData( $new, $old ) {
256
- $old = $this->db->dbname . '.' . $old;
257
- $new = $this->options->databaseDatabase . '.' . $new;
258
-
259
-
260
- $rows = $this->options->job->start + $this->settings->queryLimit;
261
- $this->log(
262
- "DB Copy: {$old} as {$new} from {$this->options->job->start} to {$rows} records"
263
- );
264
-
265
- $limitation = '';
266
-
267
- if( 0 < ( int ) $this->settings->queryLimit ) {
268
- $limitation = " LIMIT {$this->settings->queryLimit} OFFSET {$this->options->job->start}";
269
- }
270
-
271
- $this->stagingDb->query(
272
- "INSERT INTO {$new} SELECT * FROM {$old} {$limitation}"
273
- );
274
-
275
- // Set new offset
276
- $this->options->job->start += $this->settings->queryLimit;
277
- }
278
-
279
- /**
280
- * Set the job
281
- * @param string $table
282
- */
283
- private function setJob( $table ) {
284
- if( isset( $this->options->job->current ) ) {
285
- return;
286
- }
287
-
288
- $this->options->job->current = $table;
289
- $this->options->job->start = 0;
290
- }
291
-
292
- /**
293
- * Start Job
294
- * @param string $new
295
- * @param string $old
296
- * @return bool
297
- */
298
- private function startJob( $new, $old ) {
299
-
300
- if( $this->isExcludedTable( $new ) ) {
301
- return false;
302
- }
303
-
304
- $this->options->job->total = 0;
305
-
306
- $new = $this->options->databaseDatabase . '.' . $new;
307
- $old = $this->db->dbname . '.' . $old;
308
-
309
- if( 0 != $this->options->job->start ) {
310
- return true;
311
- }
312
-
313
- // Table does not exists
314
- $table = str_replace( $this->db->dbname . '.', null, $old );
315
- $result = $this->db->query( "SHOW TABLES LIKE '{$table}'" );
316
- if( !$result || 0 === $result ) {
317
- return true;
318
- }
319
-
320
- $this->log( "DB Copy: Creating table {$new}" );
321
-
322
- $this->stagingDb->query( "CREATE TABLE {$new} LIKE {$old}" );
323
-
324
- $this->options->job->total = ( int ) $this->stagingDb->get_var( "SELECT COUNT(1) FROM {$old}" );
325
-
326
- if( 0 == $this->options->job->total ) {
327
- $this->finishStep();
328
- return false;
329
- }
330
-
331
- return true;
332
- }
333
-
334
- /**
335
- * Is table excluded from search replace processing?
336
- * @param string $table
337
- * @return boolean
338
- */
339
- private function isExcludedTable( $table ) {
340
-
341
- $customTables = apply_filters( 'wpstg_clone_database_tables_exclude', array() );
342
- $defaultTables = array('blogs', 'blog_versions');
343
-
344
- $tables = array_merge( $customTables, $defaultTables );
345
-
346
- $excludedTables = array();
347
- foreach ( $tables as $key => $value ) {
348
- $excludedTables[] = $this->options->prefix . $value;
349
- }
350
-
351
- if( in_array( $table, $excludedTables ) ) {
352
- return true;
353
- }
354
- return false;
355
- }
356
-
357
- /**
358
- * Finish the step
359
- */
360
- private function finishStep() {
361
- // This job is not finished yet
362
- if( $this->options->job->total > $this->options->job->start ) {
363
- return false;
364
- }
365
-
366
- // Add it to cloned tables listing
367
- $this->options->clonedTables[] = isset( $this->options->tables[$this->options->currentStep] ) ? $this->options->tables[$this->options->currentStep] : false;
368
-
369
- // Reset job
370
- $this->options->job = new \stdClass();
371
-
372
- return true;
373
- }
374
-
375
- /**
376
- * Drop table if necessary
377
- * @param string $new
378
- */
379
- private function dropTable( $new ) {
380
- $old = $this->stagingDb->get_var( $this->stagingDb->prepare( "SHOW TABLES LIKE %s", $new ) );
381
-
382
- if( !$this->shouldDropTable( $new, $old ) ) {
383
- return;
384
- }
385
-
386
- $this->log( "DB Copy: {$new} already exists, dropping it first" );
387
- $this->stagingDb->query( "DROP TABLE {$new}" );
388
- }
389
-
390
- /**
391
- * Check if table needs to be dropped
392
- * @param string $new
393
- * @param string $old
394
- * @return bool
395
- */
396
- private function shouldDropTable( $new, $old ) {
397
-
398
-
399
-
400
- if( $old === $new &&
401
- (
402
- !isset( $this->options->job->current ) ||
403
- !isset( $this->options->job->start ) ||
404
- 0 == $this->options->job->start
405
- ) ) {
406
- return true;
407
- }
408
- return false;
409
- }
410
-
411
- }
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
+ * Initialize
38
+ */
39
+ public function initialize() {
40
+ $this->db = WPStaging::getInstance()->get( "wpdb" );
41
+ $this->stagingDb = $this->getExternalDBConnection();
42
+ $this->getTables();
43
+ // Add 2 to total table count because we need to copy two more tables from the main multisite installation wp_users and wp_usermeta
44
+ $this->total = count( $this->options->tables ) + 2;
45
+ $this->isFatalError();
46
+ }
47
+
48
+ /**
49
+ * Get external db object
50
+ * @return mixed db | false
51
+ */
52
+ private function getExternalDBConnection() {
53
+
54
+ $db = new \wpdb( $this->options->databaseUser, $this->options->databasePassword, $this->options->databaseDatabase, $this->options->databaseServer );
55
+
56
+ // Can not connect to mysql
57
+ if( !empty( $db->error->errors['db_connect_fail']['0'] ) ) {
58
+ $this->returnException( "Can not connect to external database {$this->options->databaseDatabase}" );
59
+ return false;
60
+ }
61
+
62
+ // Can not connect to database
63
+ $sql = "SHOW DATABASES LIKE '{$this->options->databaseDatabase}';";
64
+ $results = $db->query( $sql );
65
+ if( empty( $results ) ) {
66
+ $this->returnException( "Can not connect to external database {$this->options->databaseDatabase}" );
67
+ return false;
68
+ }
69
+ return $db;
70
+ }
71
+
72
+ private function getTables() {
73
+ // Add wp_users and wp_usermeta to the tables if they do not exist
74
+ // because they are not available in MU installation but we need them on the staging site
75
+
76
+ if( !in_array( $this->db->prefix . 'users', $this->options->tables ) ) {
77
+ array_push( $this->options->tables, $this->db->prefix . 'users' );
78
+ $this->saveOptions();
79
+ }
80
+ if( !in_array( $this->db->prefix . 'usermeta', $this->options->tables ) ) {
81
+ array_push( $this->options->tables, $this->db->prefix . 'usermeta' );
82
+ $this->saveOptions();
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Return fatal error and stops here if subfolder already exists
88
+ * and mainJob is not updating the clone
89
+ * @return boolean
90
+ */
91
+ private function isFatalError() {
92
+ $path = trailingslashit( get_home_path() ) . $this->options->cloneDirectoryName;
93
+ if( isset( $this->options->mainJob ) && $this->options->mainJob !== 'updating' && is_dir( $path ) ) {
94
+ $this->returnException( " Can not continue! Change the name of the clone or delete existing folder. Then try again. Folder already exists: " . $path );
95
+ }
96
+ return false;
97
+ }
98
+
99
+ /**
100
+ * Calculate Total Steps in This Job and Assign It to $this->options->totalSteps
101
+ * @return void
102
+ */
103
+ protected function calculateTotalSteps() {
104
+ $this->options->totalSteps = ($this->total === 0) ? 1 : $this->total;
105
+ }
106
+
107
+ /**
108
+ * Execute the Current Step
109
+ * Returns false when over threshold limits are hit or when the job is done, true otherwise
110
+ * @return bool
111
+ */
112
+ protected function execute() {
113
+
114
+
115
+ // Over limits threshold
116
+ if( $this->isOverThreshold() ) {
117
+ // Prepare response and save current progress
118
+ $this->prepareResponse( false, false );
119
+ $this->saveOptions();
120
+ return false;
121
+ }
122
+
123
+ // No more steps, finished
124
+ if( $this->options->currentStep > $this->total ) {
125
+ $this->prepareResponse( true, false );
126
+ return false;
127
+ }
128
+
129
+ // Copy table
130
+ if( isset( $this->options->tables[$this->options->currentStep] ) && !$this->copyTable( $this->options->tables[$this->options->currentStep] ) ) {
131
+ // Prepare Response
132
+ $this->prepareResponse( false, false );
133
+
134
+ // Not finished
135
+ return true;
136
+ }
137
+
138
+ // $this->copyWpUsers();
139
+ //
140
+ // $this->copyWpUsermeta();
141
+ // Prepare Response
142
+ $this->prepareResponse();
143
+
144
+ // Not finished
145
+ return true;
146
+ }
147
+
148
+ /**
149
+ * Get new prefix for the staging site
150
+ * @return string
151
+ */
152
+ private function getStagingPrefix() {
153
+
154
+ $this->options->prefix = !empty( $this->options->databasePrefix ) ? $this->options->databasePrefix : $this->db->prefix;
155
+
156
+ return $this->options->prefix;
157
+ }
158
+
159
+ /**
160
+ * No worries, SQL queries don't eat from PHP execution time!
161
+ * @param string $tableName
162
+ * @return bool
163
+ */
164
+ private function copyTable( $tableName ) {
165
+
166
+ $strings = new Strings();
167
+ $tableName = is_object( $tableName ) ? $tableName->name : $tableName;
168
+ $newTableName = $this->getStagingPrefix() . $strings->str_replace_first( $this->db->prefix, null, $tableName );
169
+
170
+ // Get wp_users from main site
171
+ if( 'users' === $strings->str_replace_first( $this->db->prefix, null, $tableName ) ) {
172
+ $tableName = $this->db->base_prefix . 'users';
173
+ }
174
+ // Get wp_usermeta from main site
175
+ if( 'usermeta' === $strings->str_replace_first( $this->db->prefix, null, $tableName ) ) {
176
+ $tableName = $this->db->base_prefix . 'usermeta';
177
+ }
178
+
179
+ // Drop table if necessary
180
+ $this->dropTable( $newTableName );
181
+
182
+ // Save current job
183
+ $this->setJob( $newTableName );
184
+
185
+ // Beginning of the job
186
+ if( !$this->startJob( $newTableName, $tableName ) ) {
187
+ return true;
188
+ }
189
+
190
+ // Copy data
191
+ $this->copyData( $newTableName, $tableName );
192
+
193
+ // Finish the step
194
+ return $this->finishStep();
195
+ }
196
+
197
+ /**
198
+ * Copy multisite global user table wp_users to wpstgX_users
199
+ * @return bool
200
+ */
201
+ // private function copyWpUsers() {
202
+ // $strings = new Strings();
203
+ // $tableName = $this->db->base_prefix . 'users';
204
+ // $newTableName = $this->getStagingPrefix() . $strings->str_replace_first( $this->db->base_prefix, null, $tableName );
205
+ //
206
+ // // Drop table if necessary
207
+ // $this->dropTable( $newTableName );
208
+ //
209
+ // // Save current job
210
+ // $this->setJob( $newTableName );
211
+ //
212
+ // // Beginning of the job
213
+ // if( !$this->startJob( $newTableName, $tableName ) ) {
214
+ // return true;
215
+ // }
216
+ //
217
+ // // Copy data
218
+ // $this->copyData( $newTableName, $tableName );
219
+ //
220
+ // // Finish the step
221
+ // return $this->finishStep();
222
+ // }
223
+
224
+ /**
225
+ * Copy multisite global user table wp_usermeta to wpstgX_users
226
+ * @return bool
227
+ */
228
+ // private function copyWpUsermeta() {
229
+ // $strings = new Strings();
230
+ // $tableName = $this->db->base_prefix . 'usermeta';
231
+ // $newTableName = $this->getStagingPrefix() . $strings->str_replace_first( $this->db->base_prefix, null, $tableName );
232
+ //
233
+ // // Drop table if necessary
234
+ // $this->dropTable( $newTableName );
235
+ //
236
+ // // Save current job
237
+ // $this->setJob( $newTableName );
238
+ //
239
+ // // Beginning of the job
240
+ // if( !$this->startJob( $newTableName, $tableName ) ) {
241
+ // return true;
242
+ // }
243
+ // // Copy data
244
+ // $this->copyData( $newTableName, $tableName );
245
+ //
246
+ // // Finish the step
247
+ // return $this->finishStep();
248
+ // }
249
+
250
+ /**
251
+ * Copy data from old table to new table
252
+ * @param string $new
253
+ * @param string $old
254
+ */
255
+ private function copyData( $new, $old ) {
256
+ $old = $this->db->dbname . '.' . $old;
257
+ $new = $this->options->databaseDatabase . '.' . $new;
258
+
259
+
260
+ $rows = $this->options->job->start + $this->settings->queryLimit;
261
+ $this->log(
262
+ "DB Copy: {$old} as {$new} from {$this->options->job->start} to {$rows} records"
263
+ );
264
+
265
+ $limitation = '';
266
+
267
+ if( 0 < ( int ) $this->settings->queryLimit ) {
268
+ $limitation = " LIMIT {$this->settings->queryLimit} OFFSET {$this->options->job->start}";
269
+ }
270
+
271
+ $this->stagingDb->query(
272
+ "INSERT INTO {$new} SELECT * FROM {$old} {$limitation}"
273
+ );
274
+
275
+ // Set new offset
276
+ $this->options->job->start += $this->settings->queryLimit;
277
+ }
278
+
279
+ /**
280
+ * Set the job
281
+ * @param string $table
282
+ */
283
+ private function setJob( $table ) {
284
+ if( isset( $this->options->job->current ) ) {
285
+ return;
286
+ }
287
+
288
+ $this->options->job->current = $table;
289
+ $this->options->job->start = 0;
290
+ }
291
+
292
+ /**
293
+ * Start Job
294
+ * @param string $new
295
+ * @param string $old
296
+ * @return bool
297
+ */
298
+ private function startJob( $new, $old ) {
299
+
300
+ if( $this->isExcludedTable( $new ) ) {
301
+ return false;
302
+ }
303
+
304
+ $this->options->job->total = 0;
305
+
306
+ $new = $this->options->databaseDatabase . '.' . $new;
307
+ $old = $this->db->dbname . '.' . $old;
308
+
309
+ if( 0 != $this->options->job->start ) {
310
+ return true;
311
+ }
312
+
313
+ // Table does not exists
314
+ $table = str_replace( $this->db->dbname . '.', null, $old );
315
+ $result = $this->db->query( "SHOW TABLES LIKE '{$table}'" );
316
+ if( !$result || 0 === $result ) {
317
+ return true;
318
+ }
319
+
320
+ $this->log( "DB Copy: Creating table {$new}" );
321
+
322
+ $this->stagingDb->query( "CREATE TABLE {$new} LIKE {$old}" );
323
+
324
+ $this->options->job->total = ( int ) $this->stagingDb->get_var( "SELECT COUNT(1) FROM {$old}" );
325
+
326
+ if( 0 == $this->options->job->total ) {
327
+ $this->finishStep();
328
+ return false;
329
+ }
330
+
331
+ return true;
332
+ }
333
+
334
+ /**
335
+ * Is table excluded from search replace processing?
336
+ * @param string $table
337
+ * @return boolean
338
+ */
339
+ private function isExcludedTable( $table ) {
340
+
341
+ $customTables = apply_filters( 'wpstg_clone_database_tables_exclude', array() );
342
+ $defaultTables = array('blogs', 'blog_versions');
343
+
344
+ $tables = array_merge( $customTables, $defaultTables );
345
+
346
+ $excludedTables = array();
347
+ foreach ( $tables as $key => $value ) {
348
+ $excludedTables[] = $this->options->prefix . $value;
349
+ }
350
+
351
+ if( in_array( $table, $excludedTables ) ) {
352
+ return true;
353
+ }
354
+ return false;
355
+ }
356
+
357
+ /**
358
+ * Finish the step
359
+ */
360
+ private function finishStep() {
361
+ // This job is not finished yet
362
+ if( $this->options->job->total > $this->options->job->start ) {
363
+ return false;
364
+ }
365
+
366
+ // Add it to cloned tables listing
367
+ $this->options->clonedTables[] = isset( $this->options->tables[$this->options->currentStep] ) ? $this->options->tables[$this->options->currentStep] : false;
368
+
369
+ // Reset job
370
+ $this->options->job = new \stdClass();
371
+
372
+ return true;
373
+ }
374
+
375
+ /**
376
+ * Drop table if necessary
377
+ * @param string $new
378
+ */
379
+ private function dropTable( $new ) {
380
+ $old = $this->stagingDb->get_var( $this->stagingDb->prepare( "SHOW TABLES LIKE %s", $new ) );
381
+
382
+ if( !$this->shouldDropTable( $new, $old ) ) {
383
+ return;
384
+ }
385
+
386
+ $this->log( "DB Copy: {$new} already exists, dropping it first" );
387
+ $this->stagingDb->query( "DROP TABLE {$new}" );
388
+ }
389
+
390
+ /**
391
+ * Check if table needs to be dropped
392
+ * @param string $new
393
+ * @param string $old
394
+ * @return bool
395
+ */
396
+ private function shouldDropTable( $new, $old ) {
397
+
398
+
399
+
400
+ if( $old === $new &&
401
+ (
402
+ !isset( $this->options->job->current ) ||
403
+ !isset( $this->options->job->start ) ||
404
+ 0 == $this->options->job->start
405
+ ) ) {
406
+ return true;
407
+ }
408
+ return false;
409
+ }
410
+
411
+ }
apps/Backend/Modules/Jobs/Multisite/Directories.php CHANGED
@@ -1,659 +1,659 @@
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\RecursiveFilterNewLine;
15
- use WPStaging\Iterators\RecursiveFilterExclude;
16
- use WPStaging\Backend\Modules\Jobs\JobExecutable;
17
-
18
- /**
19
- * Class Files
20
- * @package WPStaging\Backend\Modules\Directories
21
- */
22
- class Directories extends JobExecutable {
23
-
24
- /**
25
- * @var array
26
- */
27
- private $files = array();
28
-
29
- /**
30
- * Total steps to do
31
- * @var int
32
- */
33
- private $total = 5;
34
-
35
- /**
36
- * path to the cache file
37
- * @var string
38
- */
39
- private $filename;
40
-
41
- /**
42
- * Initialize
43
- */
44
- public function initialize() {
45
- $this->filename = $this->cache->getCacheDir() . "files_to_copy." . $this->cache->getCacheExtension();
46
- }
47
-
48
- /**
49
- * Calculate Total Steps in This Job and Assign It to $this->options->totalSteps
50
- * @return void
51
- */
52
- protected function calculateTotalSteps() {
53
-
54
- $this->options->totalSteps = $this->total + count( $this->options->extraDirectories );
55
- }
56
-
57
- /**
58
- * Start Module
59
- * @return object
60
- */
61
- public function start() {
62
-
63
- // Execute steps
64
- $this->run();
65
-
66
- // Save option, progress
67
- $this->saveProgress();
68
-
69
- return ( object ) $this->response;
70
- }
71
-
72
- /**
73
- * Step 0
74
- * Get WP Root files
75
- * Does not collect any sub folders
76
- */
77
- private function getWpRootFiles() {
78
-
79
- // open file handle
80
- $files = $this->open( $this->filename, 'a' );
81
-
82
-
83
- try {
84
-
85
- // Iterate over wp root directory
86
- $iterator = new \DirectoryIterator( \WPStaging\WPStaging::getWPpath() );
87
-
88
- $this->log( "Scanning / for files" );
89
-
90
- // Write path line
91
- foreach ( $iterator as $item ) {
92
- if( !$item->isDot() && $item->isFile() ) {
93
- if( $this->write( $files, $iterator->getFilename() . PHP_EOL ) ) {
94
- $this->options->totalFiles++;
95
-
96
- // Add current file size
97
- $this->options->totalFileSize += $iterator->getSize();
98
- }
99
- }
100
- }
101
- } catch ( \Exception $e ) {
102
- $this->returnException( 'Error: ' . $e->getMessage() );
103
- } catch ( \Exception $e ) {
104
- // Skip bad file permissions
105
- }
106
-
107
- $this->close( $files );
108
- return true;
109
- }
110
-
111
- /**
112
- * Step 2
113
- * Get WP Content Files without multisite folder wp-content/uploads/sites
114
- */
115
- private function getWpContentFiles() {
116
-
117
- // Skip it
118
- if( $this->isDirectoryExcluded( WP_CONTENT_DIR ) ) {
119
- $this->log( "Skip " . \WPStaging\WPStaging::getWPpath() . 'wp-content' . DIRECTORY_SEPARATOR );
120
- return true;
121
- }
122
- // open file handle
123
- $files = $this->open( $this->filename, 'a' );
124
-
125
- /**
126
- * Excluded folders relative to the folder to iterate
127
- */
128
- $excludePaths = array(
129
- 'cache',
130
- 'plugins' . DIRECTORY_SEPARATOR . 'wps-hide-login',
131
- 'uploads' . DIRECTORY_SEPARATOR . 'sites'
132
- );
133
-
134
- /**
135
- * Get user excluded folders
136
- */
137
- $directory = array();
138
- foreach ( $this->options->excludedDirectories as $dir ) {
139
- if( strpos( $dir, WP_CONTENT_DIR ) !== false ) {
140
- $directory[] = ltrim( str_replace( WP_CONTENT_DIR, '', $dir ), '/' );
141
- }
142
- }
143
-
144
- $excludePaths = array_merge( $excludePaths, $directory );
145
-
146
- // $excludeFolders = array(
147
- // 'cache',
148
- // 'node_modules',
149
- // 'nbproject',
150
- // 'wps-hide-login'
151
- // );
152
-
153
- try {
154
-
155
- // Iterate over content directory
156
- $iterator = new \WPStaging\Iterators\RecursiveDirectoryIterator( WP_CONTENT_DIR );
157
-
158
- // Exclude new line file names
159
- $iterator = new \WPStaging\Iterators\RecursiveFilterNewLine( $iterator );
160
-
161
- // Exclude sites, uploads, plugins or themes
162
- $iterator = new \WPStaging\Iterators\RecursiveFilterExclude( $iterator, apply_filters( 'wpstg_clone_mu_excl_folders', $excludePaths ) );
163
- // Recursively iterate over content directory
164
- $iterator = new \RecursiveIteratorIterator( $iterator, \RecursiveIteratorIterator::LEAVES_ONLY, \RecursiveIteratorIterator::CATCH_GET_CHILD );
165
-
166
- $this->log( "Scanning /wp-content for its sub-directories and files" );
167
-
168
- // Write path line
169
- foreach ( $iterator as $item ) {
170
- if( $item->isFile() ) {
171
- $file = 'wp-content' . DIRECTORY_SEPARATOR . $iterator->getSubPathName() . PHP_EOL;
172
- if( $this->write( $files, $file ) ) {
173
- $this->options->totalFiles++;
174
-
175
- // Add current file size
176
- $this->options->totalFileSize += $iterator->getSize();
177
- }
178
- }
179
- }
180
- } catch ( \Exception $e ) {
181
- $this->returnException( 'Error: ' . $e->getMessage() );
182
- //throw new \Exception( 'Error: ' . $e->getMessage() );
183
- } catch ( \Exception $e ) {
184
- // Skip bad file permissions
185
- }
186
-
187
- // close the file handler
188
- $this->close( $files );
189
- return true;
190
- }
191
-
192
- /**
193
- * Step 2
194
- * @return boolean
195
- * @throws \Exception
196
- */
197
- private function getWpIncludesFiles() {
198
-
199
- // Skip it
200
- if( $this->isDirectoryExcluded( \WPStaging\WPStaging::getWPpath() . 'wp-includes' . DIRECTORY_SEPARATOR ) ) {
201
- $this->log( "Skip " . \WPStaging\WPStaging::getWPpath() . 'wp-includes' . DIRECTORY_SEPARATOR );
202
- return true;
203
- }
204
-
205
- // open file handle and attach data to end of file
206
- $files = $this->open( $this->filename, 'a' );
207
-
208
- try {
209
-
210
- // Iterate over wp-admin directory
211
- $iterator = new \WPStaging\Iterators\RecursiveDirectoryIterator( \WPStaging\WPStaging::getWPpath() . 'wp-includes' . DIRECTORY_SEPARATOR );
212
-
213
- // Exclude new line file names
214
- $iterator = new \WPStaging\Iterators\RecursiveFilterNewLine( $iterator );
215
-
216
- // Recursively iterate over wp-includes directory
217
- $iterator = new \RecursiveIteratorIterator( $iterator, \RecursiveIteratorIterator::LEAVES_ONLY, \RecursiveIteratorIterator::CATCH_GET_CHILD );
218
-
219
- $this->log( "Scanning /wp-includes for its sub-directories and files" );
220
-
221
- // Write files
222
- foreach ( $iterator as $item ) {
223
- if( $item->isFile() ) {
224
- if( $this->write( $files, 'wp-includes' . DIRECTORY_SEPARATOR . $iterator->getSubPathName() . PHP_EOL ) ) {
225
- $this->options->totalFiles++;
226
-
227
- // Add current file size
228
- $this->options->totalFileSize += $iterator->getSize();
229
- }
230
- }
231
- }
232
- } catch ( \Exception $e ) {
233
- //$this->returnException('Out of disk space.');
234
- throw new \Exception( 'Error: ' . $e->getMessage() );
235
- } catch ( \Exception $e ) {
236
- // Skip bad file permissions
237
- }
238
-
239
- // close the file handler
240
- $this->close( $files );
241
- return true;
242
- }
243
-
244
- /**
245
- * Step 3
246
- * @return boolean
247
- * @throws \Exception
248
- */
249
- private function getWpAdminFiles() {
250
-
251
- // Skip it
252
- if( $this->isDirectoryExcluded( \WPStaging\WPStaging::getWPpath() . 'wp-admin' . DIRECTORY_SEPARATOR ) ) {
253
- $this->log( "Skip " . \WPStaging\WPStaging::getWPpath() . 'wp-admin' . DIRECTORY_SEPARATOR );
254
- return true;
255
- }
256
-
257
- // open file handle and attach data to end of file
258
- $files = $this->open( $this->filename, 'a' );
259
-
260
- try {
261
-
262
- // Iterate over wp-admin directory
263
- $iterator = new \WPStaging\Iterators\RecursiveDirectoryIterator( \WPStaging\WPStaging::getWPpath() . 'wp-admin' . DIRECTORY_SEPARATOR );
264
-
265
- // Exclude new line file names
266
- $iterator = new \WPStaging\Iterators\RecursiveFilterNewLine( $iterator );
267
-
268
- // Recursively iterate over content directory
269
- $iterator = new \RecursiveIteratorIterator( $iterator, \RecursiveIteratorIterator::LEAVES_ONLY, \RecursiveIteratorIterator::CATCH_GET_CHILD );
270
-
271
- $this->log( "Scanning /wp-admin for its sub-directories and files" );
272
-
273
- // Write path line
274
- foreach ( $iterator as $item ) {
275
- if( $item->isFile() ) {
276
- if( $this->write( $files, 'wp-admin' . DIRECTORY_SEPARATOR . $iterator->getSubPathName() . PHP_EOL ) ) {
277
- $this->options->totalFiles++;
278
- // Add current file size
279
- $this->options->totalFileSize += $iterator->getSize();
280
- }
281
- }
282
- }
283
- } catch ( \Exception $e ) {
284
- $this->returnException( 'Error: ' . $e->getMessage() );
285
- //throw new \Exception('Error: ' . $e->getMessage());
286
- } catch ( \Exception $e ) {
287
- // Skip bad file permissions
288
- }
289
-
290
- // close the file handler
291
- $this->close( $files );
292
- return true;
293
- }
294
-
295
- /**
296
- * Step 4
297
- * Get WP Content Uploads Files multisite folder wp-content/uploads/sites or wp-content/blogs.dir/ID/files
298
- */
299
- private function getWpContentUploadsSites() {
300
-
301
- // Skip if main site is cloned
302
- if( is_main_site() ) {
303
- return true;
304
- }
305
-
306
- $blogId = get_current_blog_id();
307
-
308
- // Absolute path to uploads folder
309
- //$path = WP_CONTENT_DIR . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'sites' . DIRECTORY_SEPARATOR . $blogId . DIRECTORY_SEPARATOR;
310
- $path = $this->getAbsUploadPath();
311
-
312
- // Skip it
313
- if( !is_dir( $path ) ) {
314
- return true;
315
- }
316
-
317
- // Skip it
318
- if( $this->isDirectoryExcluded( $path ) ) {
319
- return true;
320
- }
321
-
322
-
323
- // open file handle
324
- $files = $this->open( $this->filename, 'a' );
325
-
326
- /**
327
- * Excluded folders relative to the folder to iterate
328
- */
329
- $excludePaths = array(
330
- 'cache',
331
- 'plugins' . DIRECTORY_SEPARATOR . 'wps-hide-login',
332
- 'uploads' . DIRECTORY_SEPARATOR . 'sites'
333
- );
334
-
335
- /**
336
- * Get user excluded folders
337
- */
338
- $directory = array();
339
- foreach ( $this->options->excludedDirectories as $dir ) {
340
- if( strpos( $dir, $path ) !== false ) {
341
- $directory[] = ltrim( str_replace( $path, '', $dir ), '/' );
342
- }
343
- }
344
-
345
- $excludePaths = array_merge( $excludePaths, $directory );
346
-
347
- try {
348
-
349
- // Iterate over content directory
350
- $iterator = new \WPStaging\Iterators\RecursiveDirectoryIterator( $path );
351
-
352
- // Exclude new line file names
353
- $iterator = new \WPStaging\Iterators\RecursiveFilterNewLine( $iterator );
354
-
355
- // Exclude sites, uploads, plugins or themes
356
- $iterator = new \WPStaging\Iterators\RecursiveFilterExclude( $iterator, $excludePaths );
357
-
358
- // Recursively iterate over content directory
359
- $iterator = new \RecursiveIteratorIterator( $iterator, \RecursiveIteratorIterator::LEAVES_ONLY, \RecursiveIteratorIterator::CATCH_GET_CHILD );
360
- $this->log( "Scanning /wp-content/uploads/sites/{$blogId} for its sub-directories and files" );
361
-
362
- // Write path line
363
- foreach ( $iterator as $item ) {
364
- if( $item->isFile() ) {
365
- //$file = 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'sites' . DIRECTORY_SEPARATOR . $blogId . DIRECTORY_SEPARATOR . $iterator->getSubPathName() . PHP_EOL;
366
- $file = $this->getRelUploadPath() . $iterator->getSubPathName() . PHP_EOL;
367
- if( $this->write( $files, $file ) ) {
368
- $this->options->totalFiles++;
369
-
370
- // Add current file size
371
- $this->options->totalFileSize += $iterator->getSize();
372
- }
373
- }
374
- }
375
- } catch ( \Exception $e ) {
376
- $this->returnException( 'Error: ' . $e->getMessage() );
377
- //throw new \Exception( 'Error: ' . $e->getMessage() );
378
- } catch ( \Exception $e ) {
379
- // Skip bad file permissions
380
- }
381
-
382
- // close the file handler
383
- $this->close( $files );
384
- return true;
385
- }
386
-
387
- /**
388
- * 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/
389
- * @return type
390
- */
391
- private function getAbsUploadPath() {
392
- // Check first which method is used
393
- $uploads = wp_upload_dir();
394
- $basedir = $uploads['basedir'];
395
-
396
- return trailingslashit( $basedir );
397
-
398
- // if( false === strpos( $basedir, 'blogs.dir' ) ) {
399
- // // Since WP 3.5
400
- // return 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'sites' . DIRECTORY_SEPARATOR . get_current_blog_id();
401
- // } else {
402
- // // old blog structure
403
- // return 'wp-content' . DIRECTORY_SEPARATOR . 'blogs.dir' . DIRECTORY_SEPARATOR . get_current_blog_id() . DIRECTORY_SEPARATOR . 'files';
404
- // }
405
- }
406
-
407
- /**
408
- * Get relative path to the upload folder like wp-content/uploads or wp-content/blogs.dir/2/files
409
- * @return string
410
- */
411
- private function getRelUploadPath() {
412
- $uploads = wp_upload_dir();
413
- $basedir = $uploads['basedir'];
414
-
415
- return trailingslashit( str_replace( \WPStaging\WPStaging::getWPpath(), '', $basedir ) );
416
- }
417
-
418
- /**
419
- * Step 5 - x
420
- * Get extra folders of the wp root level
421
- * Does not collect wp-includes, wp-admin and wp-content folder
422
- */
423
- private function getExtraFiles( $folder ) {
424
-
425
- if( !is_dir( $folder ) ) {
426
- return true;
427
- }
428
-
429
- // open file handle and attach data to end of file
430
- $files = $this->open( $this->filename, 'a' );
431
-
432
- try {
433
-
434
- // Iterate over extra directory
435
- $iterator = new \WPStaging\Iterators\RecursiveDirectoryIterator( $folder );
436
-
437
- // Exclude new line file names
438
- $iterator = new \WPStaging\Iterators\RecursiveFilterNewLine( $iterator );
439
-
440
- // Exclude wp core folders
441
- // $exclude = array('wp-includes',
442
- // 'wp-admin',
443
- // 'wp-content');
444
- //
445
- // $excludeMore = array();
446
- // foreach ($this->options->excludedDirectories as $key => $value){
447
- // $excludeMore[] = $this->getLastElemAfterString('/', $value);
448
- // }
449
- //$exclude = array_merge($exclude, $excludeMore);
450
-
451
- $exclude = array();
452
-
453
- $iterator = new \WPStaging\Iterators\RecursiveFilterExclude( $iterator, $exclude );
454
- // Recursively iterate over content directory
455
- $iterator = new \RecursiveIteratorIterator( $iterator, \RecursiveIteratorIterator::LEAVES_ONLY, \RecursiveIteratorIterator::CATCH_GET_CHILD );
456
-
457
- $strings = new Strings();
458
- $this->log( "Scanning {$strings->getLastElemAfterString( '/', $folder )} for its sub-directories and files" );
459
-
460
- // Write path line
461
- foreach ( $iterator as $item ) {
462
- if( $item->isFile() ) {
463
- //if( $this->write( $files, $strings->getLastElemAfterString( '/', $folder ) . DIRECTORY_SEPARATOR . $iterator->getSubPathName() . PHP_EOL ) ) {
464
- if( $this->write( $files, str_replace( \WPStaging\WPStaging::getWPpath(), '', $folder ) . DIRECTORY_SEPARATOR . $iterator->getSubPathName() . PHP_EOL ) ) {
465
- $this->options->totalFiles++;
466
- // Add current file size
467
- $this->options->totalFileSize += $iterator->getSize();
468
- }
469
- }
470
- }
471
- } catch ( \Exception $e ) {
472
- $this->returnException( 'Error: ' . $e->getMessage() );
473
- } catch ( \Exception $e ) {
474
- // Skip bad file permissions
475
- }
476
-
477
- // close the file handler
478
- $this->close( $files );
479
- return true;
480
- }
481
-
482
- /**
483
- * Closes a file handle
484
- *
485
- * @param resource $handle File handle to close
486
- * @return boolean
487
- */
488
- public function close( $handle ) {
489
- return @fclose( $handle );
490
- }
491
-
492
- /**
493
- * Opens a file in specified mode
494
- *
495
- * @param string $file Path to the file to open
496
- * @param string $mode Mode in which to open the file
497
- * @return resource
498
- * @throws Exception
499
- */
500
- public function open( $file, $mode ) {
501
-
502
- $file_handle = @fopen( $file, $mode );
503
- if( false === $file_handle ) {
504
- $this->returnException( sprintf( __( 'Unable to open %s with mode %s', 'wp-staging' ), $file, $mode ) );
505
- //throw new Exception(sprintf(__('Unable to open %s with mode %s', 'wp-staging'), $file, $mode));
506
- }
507
-
508
- return $file_handle;
509
- }
510
-
511
- /**
512
- * Write contents to a file
513
- *
514
- * @param resource $handle File handle to write to
515
- * @param string $content Contents to write to the file
516
- * @return integer
517
- * @throws Exception
518
- * @throws Exception
519
- */
520
- public function write( $handle, $content ) {
521
- $write_result = @fwrite( $handle, $content );
522
- if( false === $write_result ) {
523
- if( ( $meta = \stream_get_meta_data( $handle ) ) ) {
524
- //$this->returnException(sprintf(__('Unable to write to: %s', 'wp-staging'), $meta['uri']));
525
- throw new \Exception( sprintf( __( 'Unable to write to: %s', 'wp-staging' ), $meta['uri'] ) );
526
- }
527
- } elseif( strlen( $content ) !== $write_result ) {
528
- //$this->returnException(__('Out of disk space.', 'wp-staging'));
529
- throw new \Exception( __( 'Out of disk space.', 'wp-staging' ) );
530
- }
531
-
532
- return $write_result;
533
- }
534
-
535
- /**
536
- * Execute the Current Step
537
- * Returns false when over threshold limits are hit or when the job is done, true otherwise
538
- * @return bool
539
- */
540
- protected function execute() {
541
-
542
- // No job left to execute
543
- if( $this->isFinished() ) {
544
- $this->prepareResponse( true, false );
545
- return false;
546
- }
547
-
548
-
549
- if( $this->options->currentStep == 0 ) {
550
- $this->getWpRootFiles();
551
- $this->prepareResponse( false, true );
552
- return false;
553
- }
554
-
555
- if( $this->options->currentStep == 1 ) {
556
- $this->getWpContentFiles();
557
- $this->prepareResponse( false, true );
558
- return false;
559
- }
560
-
561
- if( $this->options->currentStep == 2 ) {
562
- $this->getWpIncludesFiles();
563
- $this->prepareResponse( false, true );
564
- return false;
565
- }
566
-
567
- if( $this->options->currentStep == 3 ) {
568
- $this->getWpAdminFiles();
569
- $this->prepareResponse( false, true );
570
- return false;
571
- }
572
-
573
- if( $this->options->currentStep == 4 ) {
574
- $this->getWpContentUploadsSites();
575
- $this->prepareResponse( false, true );
576
- return false;
577
- }
578
-
579
- if( isset( $this->options->extraDirectories[$this->options->currentStep - $this->total] ) ) {
580
- $this->getExtraFiles( $this->options->extraDirectories[$this->options->currentStep - $this->total] );
581
- $this->prepareResponse( false, true );
582
- return false;
583
- }
584
-
585
-
586
- // Prepare response
587
- $this->prepareResponse( false, true );
588
- // Not finished
589
- return true;
590
- }
591
-
592
- /**
593
- * Checks Whether There is Any Job to Execute or Not
594
- * @return bool
595
- */
596
- protected function isFinished() {
597
- if( $this->options->currentStep >= $this->options->totalSteps ) {
598
- return true;
599
- }
600
-
601
- // return (
602
- // //$this->options->currentStep > $this->total ||
603
- // $this->options->currentStep >= $this->options->totalSteps
604
- // );
605
- }
606
-
607
- /**
608
- * Save files
609
- * @return bool
610
- */
611
- protected function saveProgress() {
612
- return $this->saveOptions();
613
- }
614
-
615
- /**
616
- * Get files
617
- * @return void
618
- */
619
- protected function getFiles() {
620
- $fileName = $this->cache->getCacheDir() . "files_to_copy." . $this->cache->getCacheExtension();
621
-
622
- if( false === ($this->files = @file_get_contents( $fileName )) ) {
623
- $this->files = array();
624
- return;
625
- }
626
-
627
- $this->files = explode( PHP_EOL, $this->files );
628
- }
629
-
630
- /**
631
- * Replace forward slash with current directory separator
632
- *
633
- * @param string $path Path
634
- *
635
- * @return string
636
- */
637
- private function sanitizeDirectorySeparator( $path ) {
638
- $string = str_replace( "/", "\\", $path );
639
- return str_replace( '\\\\', '\\', $string );
640
- }
641
-
642
- /**
643
- * Check if directory is excluded
644
- * @param string $directory
645
- * @return bool
646
- */
647
- protected function isDirectoryExcluded( $directory ) {
648
- $directory = $this->sanitizeDirectorySeparator( $directory );
649
- foreach ( $this->options->excludedDirectories as $excludedDirectory ) {
650
- $excludedDirectory = $this->sanitizeDirectorySeparator( $excludedDirectory );
651
- if( strpos( trailingslashit( $directory ), trailingslashit( $excludedDirectory ) ) === 0 ) {
652
- return true;
653
- }
654
- }
655
-
656
- return false;
657
- }
658
-
659
- }
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\RecursiveFilterNewLine;
15
+ use WPStaging\Iterators\RecursiveFilterExclude;
16
+ use WPStaging\Backend\Modules\Jobs\JobExecutable;
17
+
18
+ /**
19
+ * Class Files
20
+ * @package WPStaging\Backend\Modules\Directories
21
+ */
22
+ class Directories extends JobExecutable {
23
+
24
+ /**
25
+ * @var array
26
+ */
27
+ private $files = array();
28
+
29
+ /**
30
+ * Total steps to do
31
+ * @var int
32
+ */
33
+ private $total = 5;
34
+
35
+ /**
36
+ * path to the cache file
37
+ * @var string
38
+ */
39
+ private $filename;
40
+
41
+ /**
42
+ * Initialize
43
+ */
44
+ public function initialize() {
45
+ $this->filename = $this->cache->getCacheDir() . "files_to_copy." . $this->cache->getCacheExtension();
46
+ }
47
+
48
+ /**
49
+ * Calculate Total Steps in This Job and Assign It to $this->options->totalSteps
50
+ * @return void
51
+ */
52
+ protected function calculateTotalSteps() {
53
+
54
+ $this->options->totalSteps = $this->total + count( $this->options->extraDirectories );
55
+ }
56
+
57
+ /**
58
+ * Start Module
59
+ * @return object
60
+ */
61
+ public function start() {
62
+
63
+ // Execute steps
64
+ $this->run();
65
+
66
+ // Save option, progress
67
+ $this->saveProgress();
68
+
69
+ return ( object ) $this->response;
70
+ }
71
+
72
+ /**
73
+ * Step 0
74
+ * Get WP Root files
75
+ * Does not collect any sub folders
76
+ */
77
+ private function getWpRootFiles() {
78
+
79
+ // open file handle
80
+ $files = $this->open( $this->filename, 'a' );
81
+
82
+
83
+ try {
84
+
85
+ // Iterate over wp root directory
86
+ $iterator = new \DirectoryIterator( \WPStaging\WPStaging::getWPpath() );
87
+
88
+ $this->log( "Scanning / for files" );
89
+
90
+ // Write path line
91
+ foreach ( $iterator as $item ) {
92
+ if( !$item->isDot() && $item->isFile() ) {
93
+ if( $this->write( $files, $iterator->getFilename() . PHP_EOL ) ) {
94
+ $this->options->totalFiles++;
95
+
96
+ // Add current file size
97
+ $this->options->totalFileSize += $iterator->getSize();
98
+ }
99
+ }
100
+ }
101
+ } catch ( \Exception $e ) {
102
+ $this->returnException( 'Error: ' . $e->getMessage() );
103
+ } catch ( \Exception $e ) {
104
+ // Skip bad file permissions
105
+ }
106
+
107
+ $this->close( $files );
108
+ return true;
109
+ }
110
+
111
+ /**
112
+ * Step 2
113
+ * Get WP Content Files without multisite folder wp-content/uploads/sites
114
+ */
115
+ private function getWpContentFiles() {
116
+
117
+ // Skip it
118
+ if( $this->isDirectoryExcluded( WP_CONTENT_DIR ) ) {
119
+ $this->log( "Skip " . \WPStaging\WPStaging::getWPpath() . 'wp-content' . DIRECTORY_SEPARATOR );
120
+ return true;
121
+ }
122
+ // open file handle
123
+ $files = $this->open( $this->filename, 'a' );
124
+
125
+ /**
126
+ * Excluded folders relative to the folder to iterate
127
+ */
128
+ $excludePaths = array(
129
+ 'cache',
130
+ 'plugins' . DIRECTORY_SEPARATOR . 'wps-hide-login',
131
+ 'uploads' . DIRECTORY_SEPARATOR . 'sites'
132
+ );
133
+
134
+ /**
135
+ * Get user excluded folders
136
+ */
137
+ $directory = array();
138
+ foreach ( $this->options->excludedDirectories as $dir ) {
139
+ if( strpos( $dir, WP_CONTENT_DIR ) !== false ) {
140
+ $directory[] = ltrim( str_replace( WP_CONTENT_DIR, '', $dir ), '/' );
141
+ }
142
+ }
143
+
144
+ $excludePaths = array_merge( $excludePaths, $directory );
145
+
146
+ // $excludeFolders = array(
147
+ // 'cache',
148
+ // 'node_modules',
149
+ // 'nbproject',
150
+ // 'wps-hide-login'
151
+ // );
152
+
153
+ try {
154
+
155
+ // Iterate over content directory
156
+ $iterator = new \WPStaging\Iterators\RecursiveDirectoryIterator( WP_CONTENT_DIR );
157
+
158
+ // Exclude new line file names
159
+ $iterator = new \WPStaging\Iterators\RecursiveFilterNewLine( $iterator );
160
+
161
+ // Exclude sites, uploads, plugins or themes
162
+ $iterator = new \WPStaging\Iterators\RecursiveFilterExclude( $iterator, apply_filters( 'wpstg_clone_mu_excl_folders', $excludePaths ) );
163
+ // Recursively iterate over content directory
164
+ $iterator = new \RecursiveIteratorIterator( $iterator, \RecursiveIteratorIterator::LEAVES_ONLY, \RecursiveIteratorIterator::CATCH_GET_CHILD );
165
+
166
+ $this->log( "Scanning /wp-content for its sub-directories and files" );
167
+
168
+ // Write path line
169
+ foreach ( $iterator as $item ) {
170
+ if( $item->isFile() ) {
171
+ $file = 'wp-content' . DIRECTORY_SEPARATOR . $iterator->getSubPathName() . PHP_EOL;
172
+ if( $this->write( $files, $file ) ) {
173
+ $this->options->totalFiles++;
174
+
175
+ // Add current file size
176
+ $this->options->totalFileSize += $iterator->getSize();
177
+ }
178
+ }
179
+ }
180
+ } catch ( \Exception $e ) {
181
+ $this->returnException( 'Error: ' . $e->getMessage() );
182
+ //throw new \Exception( 'Error: ' . $e->getMessage() );
183
+ } catch ( \Exception $e ) {
184
+ // Skip bad file permissions
185
+ }
186
+
187
+ // close the file handler
188
+ $this->close( $files );
189
+ return true;
190
+ }
191
+
192
+ /**
193
+ * Step 2
194
+ * @return boolean
195
+ * @throws \Exception
196
+ */
197
+ private function getWpIncludesFiles() {
198
+
199
+ // Skip it
200
+ if( $this->isDirectoryExcluded( \WPStaging\WPStaging::getWPpath() . 'wp-includes' . DIRECTORY_SEPARATOR ) ) {
201
+ $this->log( "Skip " . \WPStaging\WPStaging::getWPpath() . 'wp-includes' . DIRECTORY_SEPARATOR );
202
+ return true;
203
+ }
204
+
205
+ // open file handle and attach data to end of file
206
+ $files = $this->open( $this->filename, 'a' );
207
+
208
+ try {
209
+
210
+ // Iterate over wp-admin directory
211
+ $iterator = new \WPStaging\Iterators\RecursiveDirectoryIterator( \WPStaging\WPStaging::getWPpath() . 'wp-includes' . DIRECTORY_SEPARATOR );
212
+
213
+ // Exclude new line file names
214
+ $iterator = new \WPStaging\Iterators\RecursiveFilterNewLine( $iterator );
215
+
216
+ // Recursively iterate over wp-includes directory
217
+ $iterator = new \RecursiveIteratorIterator( $iterator, \RecursiveIteratorIterator::LEAVES_ONLY, \RecursiveIteratorIterator::CATCH_GET_CHILD );
218
+
219
+ $this->log( "Scanning /wp-includes for its sub-directories and files" );
220
+
221
+ // Write files
222
+ foreach ( $iterator as $item ) {
223
+ if( $item->isFile() ) {
224
+ if( $this->write( $files, 'wp-includes' . DIRECTORY_SEPARATOR . $iterator->getSubPathName() . PHP_EOL ) ) {
225
+ $this->options->totalFiles++;
226
+
227
+ // Add current file size
228
+ $this->options->totalFileSize += $iterator->getSize();
229
+ }
230
+ }
231
+ }
232
+ } catch ( \Exception $e ) {
233
+ //$this->returnException('Out of disk space.');
234
+ throw new \Exception( 'Error: ' . $e->getMessage() );
235
+ } catch ( \Exception $e ) {
236
+ // Skip bad file permissions
237
+ }
238
+
239
+ // close the file handler
240
+ $this->close( $files );
241
+ return true;
242
+ }
243
+
244
+ /**
245
+ * Step 3
246
+ * @return boolean
247
+ * @throws \Exception
248
+ */
249
+ private function getWpAdminFiles() {
250
+
251
+ // Skip it
252
+ if( $this->isDirectoryExcluded( \WPStaging\WPStaging::getWPpath() . 'wp-admin' . DIRECTORY_SEPARATOR ) ) {
253
+ $this->log( "Skip " . \WPStaging\WPStaging::getWPpath() . 'wp-admin' . DIRECTORY_SEPARATOR );
254
+ return true;
255
+ }
256
+
257
+ // open file handle and attach data to end of file
258
+ $files = $this->open( $this->filename, 'a' );
259
+
260
+ try {
261
+
262
+ // Iterate over wp-admin directory
263
+ $iterator = new \WPStaging\Iterators\RecursiveDirectoryIterator( \WPStaging\WPStaging::getWPpath() . 'wp-admin' . DIRECTORY_SEPARATOR );
264
+
265
+ // Exclude new line file names
266
+ $iterator = new \WPStaging\Iterators\RecursiveFilterNewLine( $iterator );
267
+
268
+ // Recursively iterate over content directory
269
+ $iterator = new \RecursiveIteratorIterator( $iterator, \RecursiveIteratorIterator::LEAVES_ONLY, \RecursiveIteratorIterator::CATCH_GET_CHILD );
270
+
271
+ $this->log( "Scanning /wp-admin for its sub-directories and files" );
272
+
273
+ // Write path line
274
+ foreach ( $iterator as $item ) {
275
+ if( $item->isFile() ) {
276
+ if( $this->write( $files, 'wp-admin' . DIRECTORY_SEPARATOR . $iterator->getSubPathName() . PHP_EOL ) ) {
277
+ $this->options->totalFiles++;
278
+ // Add current file size
279
+ $this->options->totalFileSize += $iterator->getSize();
280
+ }
281
+ }
282
+ }
283
+ } catch ( \Exception $e ) {
284
+ $this->returnException( 'Error: ' . $e->getMessage() );
285
+ //throw new \Exception('Error: ' . $e->getMessage());
286
+ } catch ( \Exception $e ) {
287
+ // Skip bad file permissions
288
+ }
289
+
290
+ // close the file handler
291
+ $this->close( $files );
292
+ return true;
293
+ }
294
+
295
+ /**
296
+ * Step 4
297
+ * Get WP Content Uploads Files multisite folder wp-content/uploads/sites or wp-content/blogs.dir/ID/files
298
+ */
299
+ private function getWpContentUploadsSites() {
300
+
301
+ // Skip if main site is cloned
302
+ if( is_main_site() ) {
303
+ return true;
304
+ }
305
+
306
+ $blogId = get_current_blog_id();
307
+
308
+ // Absolute path to uploads folder
309
+ //$path = WP_CONTENT_DIR . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'sites' . DIRECTORY_SEPARATOR . $blogId . DIRECTORY_SEPARATOR;
310
+ $path = $this->getAbsUploadPath();
311
+
312
+ // Skip it
313
+ if( !is_dir( $path ) ) {
314
+ return true;
315
+ }
316
+
317
+ // Skip it
318
+ if( $this->isDirectoryExcluded( $path ) ) {
319
+ return true;
320
+ }
321
+
322
+
323
+ // open file handle
324
+ $files = $this->open( $this->filename, 'a' );
325
+
326
+ /**
327
+ * Excluded folders relative to the folder to iterate
328
+ */
329
+ $excludePaths = array(
330
+ 'cache',
331
+ 'plugins' . DIRECTORY_SEPARATOR . 'wps-hide-login',
332
+ 'uploads' . DIRECTORY_SEPARATOR . 'sites'
333
+ );
334
+
335
+ /**
336
+ * Get user excluded folders
337
+ */
338
+ $directory = array();
339
+ foreach ( $this->options->excludedDirectories as $dir ) {
340
+ if( strpos( $dir, $path ) !== false ) {
341
+ $directory[] = ltrim( str_replace( $path, '', $dir ), '/' );
342
+ }
343
+ }
344
+
345
+ $excludePaths = array_merge( $excludePaths, $directory );
346
+
347
+ try {
348
+
349
+ // Iterate over content directory
350
+ $iterator = new \WPStaging\Iterators\RecursiveDirectoryIterator( $path );
351
+
352
+ // Exclude new line file names
353
+ $iterator = new \WPStaging\Iterators\RecursiveFilterNewLine( $iterator );
354
+
355
+ // Exclude sites, uploads, plugins or themes
356
+ $iterator = new \WPStaging\Iterators\RecursiveFilterExclude( $iterator, $excludePaths );
357
+
358
+ // Recursively iterate over content directory
359
+ $iterator = new \RecursiveIteratorIterator( $iterator, \RecursiveIteratorIterator::LEAVES_ONLY, \RecursiveIteratorIterator::CATCH_GET_CHILD );
360
+ $this->log( "Scanning /wp-content/uploads/sites/{$blogId} for its sub-directories and files" );
361
+
362
+ // Write path line
363
+ foreach ( $iterator as $item ) {
364
+ if( $item->isFile() ) {
365
+ //$file = 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'sites' . DIRECTORY_SEPARATOR . $blogId . DIRECTORY_SEPARATOR . $iterator->getSubPathName() . PHP_EOL;
366
+ $file = $this->getRelUploadPath() . $iterator->getSubPathName() . PHP_EOL;
367
+ if( $this->write( $files, $file ) ) {
368
+ $this->options->totalFiles++;
369
+
370
+ // Add current file size
371
+ $this->options->totalFileSize += $iterator->getSize();
372
+ }
373
+ }
374
+ }
375
+ } catch ( \Exception $e ) {
376
+ $this->returnException( 'Error: ' . $e->getMessage() );
377
+ //throw new \Exception( 'Error: ' . $e->getMessage() );
378
+ } catch ( \Exception $e ) {
379
+ // Skip bad file permissions
380
+ }
381
+
382
+ // close the file handler
383
+ $this->close( $files );
384
+ return true;
385
+ }
386
+
387
+ /**
388
+ * 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/
389
+ * @return type
390
+ */
391
+ private function getAbsUploadPath() {
392
+ // Check first which method is used
393
+ $uploads = wp_upload_dir();
394
+ $basedir = $uploads['basedir'];
395
+
396
+ return trailingslashit( $basedir );
397
+
398
+ // if( false === strpos( $basedir, 'blogs.dir' ) ) {
399
+ // // Since WP 3.5
400
+ // return 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'sites' . DIRECTORY_SEPARATOR . get_current_blog_id();
401
+ // } else {
402
+ // // old blog structure
403
+ // return 'wp-content' . DIRECTORY_SEPARATOR . 'blogs.dir' . DIRECTORY_SEPARATOR . get_current_blog_id() . DIRECTORY_SEPARATOR . 'files';
404
+ // }
405
+ }
406
+
407
+ /**
408
+ * Get relative path to the upload folder like wp-content/uploads or wp-content/blogs.dir/2/files
409
+ * @return string
410
+ */
411
+ private function getRelUploadPath() {
412
+ $uploads = wp_upload_dir();
413
+ $basedir = $uploads['basedir'];
414
+
415
+ return trailingslashit( str_replace( \WPStaging\WPStaging::getWPpath(), '', $basedir ) );
416
+ }
417
+
418
+ /**
419
+ * Step 5 - x
420
+ * Get extra folders of the wp root level
421
+ * Does not collect wp-includes, wp-admin and wp-content folder
422
+ */
423
+ private function getExtraFiles( $folder ) {
424
+
425
+ if( !is_dir( $folder ) ) {
426
+ return true;
427
+ }
428
+
429
+ // open file handle and attach data to end of file
430
+ $files = $this->open( $this->filename, 'a' );
431
+
432
+ try {
433
+
434
+ // Iterate over extra directory
435
+ $iterator = new \WPStaging\Iterators\RecursiveDirectoryIterator( $folder );
436
+
437
+ // Exclude new line file names
438
+ $iterator = new \WPStaging\Iterators\RecursiveFilterNewLine( $iterator );
439
+
440
+ // Exclude wp core folders
441
+ // $exclude = array('wp-includes',
442
+ // 'wp-admin',
443
+ // 'wp-content');
444
+ //
445
+ // $excludeMore = array();
446
+ // foreach ($this->options->excludedDirectories as $key => $value){
447
+ // $excludeMore[] = $this->getLastElemAfterString('/', $value);
448
+ // }
449
+ //$exclude = array_merge($exclude, $excludeMore);
450
+
451
+ $exclude = array();
452
+
453
+ $iterator = new \WPStaging\Iterators\RecursiveFilterExclude( $iterator, $exclude );
454
+ // Recursively iterate over content directory
455
+ $iterator = new \RecursiveIteratorIterator( $iterator, \RecursiveIteratorIterator::LEAVES_ONLY, \RecursiveIteratorIterator::CATCH_GET_CHILD );
456
+
457
+ $strings = new Strings();
458
+ $this->log( "Scanning {$strings->getLastElemAfterString( '/', $folder )} for its sub-directories and files" );
459
+
460
+ // Write path line
461
+ foreach ( $iterator as $item ) {
462
+ if( $item->isFile() ) {
463
+ //if( $this->write( $files, $strings->getLastElemAfterString( '/', $folder ) . DIRECTORY_SEPARATOR . $iterator->getSubPathName() . PHP_EOL ) ) {
464
+ if( $this->write( $files, str_replace( \WPStaging\WPStaging::getWPpath(), '', $folder ) . DIRECTORY_SEPARATOR . $iterator->getSubPathName() . PHP_EOL ) ) {
465
+ $this->options->totalFiles++;
466
+ // Add current file size
467
+ $this->options->totalFileSize += $iterator->getSize();
468
+ }
469
+ }
470
+ }
471
+ } catch ( \Exception $e ) {
472
+ $this->returnException( 'Error: ' . $e->getMessage() );
473
+ } catch ( \Exception $e ) {
474
+ // Skip bad file permissions
475
+ }
476
+
477
+ // close the file handler
478
+ $this->close( $files );
479
+ return true;
480
+ }
481
+
482
+ /**
483
+ * Closes a file handle
484
+ *
485
+ * @param resource $handle File handle to close
486
+ * @return boolean
487
+ */
488
+ public function close( $handle ) {
489
+ return @fclose( $handle );
490
+ }
491
+
492
+ /**
493
+ * Opens a file in specified mode
494
+ *
495
+ * @param string $file Path to the file to open
496
+ * @param string $mode Mode in which to open the file
497
+ * @return resource
498
+ * @throws Exception
499
+ */
500
+ public function open( $file, $mode ) {
501
+
502
+ $file_handle = @fopen( $file, $mode );
503
+ if( false === $file_handle ) {
504
+ $this->returnException( sprintf( __( 'Unable to open %s with mode %s', 'wp-staging' ), $file, $mode ) );
505
+ //throw new Exception(sprintf(__('Unable to open %s with mode %s', 'wp-staging'), $file, $mode));
506
+ }
507
+
508
+ return $file_handle;
509
+ }
510
+
511
+ /**
512
+ * Write contents to a file
513
+ *
514
+ * @param resource $handle File handle to write to
515
+ * @param string $content Contents to write to the file
516
+ * @return integer
517
+ * @throws Exception
518
+ * @throws Exception
519
+ */
520
+ public function write( $handle, $content ) {
521
+ $write_result = @fwrite( $handle, $content );
522
+ if( false === $write_result ) {
523
+ if( ( $meta = \stream_get_meta_data( $handle ) ) ) {
524
+ //$this->returnException(sprintf(__('Unable to write to: %s', 'wp-staging'), $meta['uri']));
525
+ throw new \Exception( sprintf( __( 'Unable to write to: %s', 'wp-staging' ), $meta['uri'] ) );
526
+ }
527
+ } elseif( strlen( $content ) !== $write_result ) {
528
+ //$this->returnException(__('Out of disk space.', 'wp-staging'));
529
+ throw new \Exception( __( 'Out of disk space.', 'wp-staging' ) );
530
+ }
531
+
532
+ return $write_result;
533
+ }
534
+
535
+ /**
536
+ * Execute the Current Step
537
+ * Returns false when over threshold limits are hit or when the job is done, true otherwise
538
+ * @return bool
539
+ */
540
+ protected function execute() {
541
+
542
+ // No job left to execute
543
+ if( $this->isFinished() ) {
544
+ $this->prepareResponse( true, false );
545
+ return false;
546
+ }
547
+
548
+
549
+ if( $this->options->currentStep == 0 ) {
550
+ $this->getWpRootFiles();
551
+ $this->prepareResponse( false, true );
552
+ return false;
553
+ }
554
+
555
+ if( $this->options->currentStep == 1 ) {
556
+ $this->getWpContentFiles();
557
+ $this->prepareResponse( false, true );
558
+ return false;
559
+ }
560
+
561
+ if( $this->options->currentStep == 2 ) {
562
+ $this->getWpIncludesFiles();
563
+ $this->prepareResponse( false, true );
564
+ return false;
565
+ }
566
+
567
+ if( $this->options->currentStep == 3 ) {
568
+ $this->getWpAdminFiles();
569
+ $this->prepareResponse( false, true );
570
+ return false;
571
+ }
572
+
573
+ if( $this->options->currentStep == 4 ) {
574
+ $this->getWpContentUploadsSites();
575
+ $this->prepareResponse( false, true );
576
+ return false;
577
+ }
578
+
579
+ if( isset( $this->options->extraDirectories[$this->options->currentStep - $this->total] ) ) {
580
+ $this->getExtraFiles( $this->options->extraDirectories[$this->options->currentStep - $this->total] );
581
+ $this->prepareResponse( false, true );
582
+ return false;
583
+ }
584
+
585
+
586
+ // Prepare response
587
+ $this->prepareResponse( false, true );
588
+ // Not finished
589
+ return true;
590
+ }
591
+
592
+ /**
593
+ * Checks Whether There is Any Job to Execute or Not
594
+ * @return bool
595
+ */
596
+ protected function isFinished() {
597
+ if( $this->options->currentStep >= $this->options->totalSteps ) {
598
+ return true;
599
+ }
600
+
601
+ // return (
602
+ // //$this->options->currentStep > $this->total ||
603
+ // $this->options->currentStep >= $this->options->totalSteps
604
+ // );
605
+ }
606
+
607
+ /**
608
+ * Save files
609
+ * @return bool
610
+ */
611
+ protected function saveProgress() {
612
+ return $this->saveOptions();
613
+ }
614
+
615
+ /**
616
+ * Get files
617
+ * @return void
618
+ */
619
+ protected function getFiles() {
620
+ $fileName = $this->cache->getCacheDir() . "files_to_copy." . $this->cache->getCacheExtension();
621
+
622
+ if( false === ($this->files = @file_get_contents( $fileName )) ) {
623
+ $this->files = array();
624
+ return;
625
+ }
626
+
627
+ $this->files = explode( PHP_EOL, $this->files );
628
+ }
629
+
630
+ /**
631
+ * Replace forward slash with current directory separator
632
+ *
633
+ * @param string $path Path
634
+ *
635
+ * @return string
636
+ */
637
+ private function sanitizeDirectorySeparator( $path ) {
638
+ $string = str_replace( "/", "\\", $path );
639
+ return str_replace( '\\\\', '\\', $string );
640
+ }
641
+
642
+ /**
643
+ * Check if directory is excluded
644
+ * @param string $directory
645
+ * @return bool
646
+ */
647
+ protected function isDirectoryExcluded( $directory ) {
648
+ $directory = $this->sanitizeDirectorySeparator( $directory );
649
+ foreach ( $this->options->excludedDirectories as $excludedDirectory ) {
650
+ $excludedDirectory = $this->sanitizeDirectorySeparator( $excludedDirectory );
651
+ if( strpos( trailingslashit( $directory ), trailingslashit( $excludedDirectory ) ) === 0 ) {
652
+ return true;
653
+ }
654
+ }
655
+
656
+ return false;
657
+ }
658
+
659
+ }
apps/Backend/Modules/Jobs/Multisite/Files.php CHANGED
@@ -1,431 +1,431 @@
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
- // if( false !== strpos( $file, 'index.php' ) ) {
139
- // $test = $file;
140
- // }
141
- $this->copyFile( $file );
142
- }
143
-
144
-
145
-
146
- $totalFiles = $this->options->copiedFiles;
147
- // Log this only every 50 entries to keep the log small and to not block the rendering browser
148
- if( $this->options->copiedFiles % 50 == 0 ) {
149
- $this->log( "Total {$totalFiles} files processed" );
150
- }
151
-
152
- return true;
153
- }
154
-
155
- /**
156
- * Checks Whether There is Any Job to Execute or Not
157
- * @return bool
158
- */
159
- private function isFinished() {
160
- return (
161
- $this->options->currentStep > $this->options->totalSteps ||
162
- $this->options->copiedFiles >= $this->options->totalFiles
163
- );
164
- }
165
-
166
- /**
167
- * @param string $file
168
- * @return bool
169
- */
170
- private function copyFile( $file ) {
171
-
172
- $file = trim( \WPStaging\WPStaging::getWPpath() . $file );
173
-
174
- $directory = dirname( $file );
175
-
176
- // Directory is excluded
177
- if( $this->isDirectoryExcluded( $directory ) ) {
178
- $this->debugLog( "Skipping directory by rule: {$file}", Logger::TYPE_INFO );
179
- return false;
180
- }
181
-
182
- // File is excluded
183
- if( $this->isFileExcluded( $file ) ) {
184
- $this->debugLog( "Skipping file by rule: {$file}", Logger::TYPE_INFO );
185
- return false;
186
- }
187
- // Path + File is excluded
188
- if( $this->isFileExcludedFullPath( $file ) ) {
189
- $this->debugLog( "Skipping file by rule: {$file}", Logger::TYPE_INFO );
190
- return false;
191
- }
192
-
193
- // Invalid file, skipping it as if succeeded
194
- if( !is_file( $file ) ) {
195
- $this->log( "File doesn't exist {$file}", Logger::TYPE_WARNING );
196
- return true;
197
- }
198
- // Invalid file, skipping it as if succeeded
199
- if( !is_readable( $file ) ) {
200
- $this->log( "Can't read file {$file}", Logger::TYPE_WARNING );
201
- return true;
202
- }
203
-
204
-
205
- // Get file size
206
- $fileSize = filesize( $file );
207
-
208
- // File is over maximum allowed file size (8MB)
209
- if( $fileSize >= $this->settings->maxFileSize * 1000000 ) {
210
- $this->log( "Skipping big file: {$file}", Logger::TYPE_INFO );
211
- return false;
212
- }
213
-
214
- // Failed to get destination
215
- if( false === ($destination = $this->getDestination( $file )) ) {
216
- $this->log( "Can't get the destination of {$file}", Logger::TYPE_WARNING );
217
- return false;
218
- }
219
-
220
- // File is over batch size
221
- if( $fileSize >= $this->settings->batchSize ) {
222
- $this->log( "Trying to copy big file: {$file} -> {$destination}", Logger::TYPE_INFO );
223
- return $this->copyBig( $file, $destination, $this->settings->batchSize );
224
- }
225
-
226
- // Attempt to copy
227
- if( !@copy( $file, $destination ) ) {
228
- $errors = error_get_last();
229
- $this->log( "Files: Failed to copy file to destination. Error: {$errors['message']} {$file} -> {$destination}", Logger::TYPE_ERROR );
230
- return false;
231
- }
232
-
233
- // Set file permissions
234
- @chmod( $file, wpstg_get_permissions_for_file() );
235
-
236
- $this->setDirPermissions( $file );
237
-
238
- return true;
239
- }
240
-
241
- /**
242
- * Set directory permissions
243
- * @param type $file
244
- * @return boolean
245
- */
246
- private function setDirPermissions( $file ) {
247
- $dir = dirname( $file );
248
- if( is_dir( $dir ) ) {
249
- @chmod( $dir, wpstg_get_permissions_for_directory() );
250
- }
251
- return false;
252
- }
253
-
254
- /**
255
- * Gets destination file and checks if the directory exists, if it does not attempts to create it.
256
- * If creating destination directory fails, it returns false, gives destination full path otherwise
257
- * @param string $file
258
- * @return bool|string
259
- */
260
- private function getDestination( $file ) {
261
- $file = $this->getMultisiteUploadFolder( $file );
262
- $relativePath = str_replace( \WPStaging\WPStaging::getWPpath(), null, $file );
263
- $destinationPath = $this->destination . $relativePath;
264
- $destinationDirectory = dirname( $destinationPath );
265
-
266
- if( !is_dir( $destinationDirectory ) && !@mkdir( $destinationDirectory, wpstg_get_permissions_for_directory(), true ) ) {
267
- $this->log( "Files: Can not create directory {$destinationDirectory}", Logger::TYPE_ERROR );
268
- return false;
269
- }
270
-
271
- return $this->sanitizeDirectorySeparator( $destinationPath );
272
- }
273
-
274
- /**
275
- * Replace relative path of file if its located in multisite upload folder
276
- * wp-content/uploads/sites/SITEID or old wordpress structure wp-content/blogs.dir/SITEID/files
277
- * @return boolean
278
- */
279
- private function getMultisiteUploadFolder( $file ) {
280
- // Check first which method is used
281
- $uploads = wp_upload_dir();
282
- $basedir = $uploads['basedir'];
283
-
284
- if( false === strpos( $basedir, 'blogs.dir' ) ) {
285
- // Since WP 3.5
286
- $search = 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'sites' . DIRECTORY_SEPARATOR . get_current_blog_id();
287
- $replace = 'wp-content' . DIRECTORY_SEPARATOR . 'uploads';
288
- $uploadsFolder = str_replace( $search, $replace, $file );
289
- } else {
290
- // old blog structure
291
- $search = 'wp-content' . DIRECTORY_SEPARATOR . 'blogs.dir' . DIRECTORY_SEPARATOR . get_current_blog_id() . DIRECTORY_SEPARATOR . 'files';
292
- $replace = 'wp-content' . DIRECTORY_SEPARATOR . 'uploads';
293
- $uploadsFolder = str_replace( $search, $replace, $file );
294
- }
295
-
296
- return $uploadsFolder;
297
- }
298
-
299
- /**
300
- * Copy bigger files than $this->settings->batchSize
301
- * @param string $src
302
- * @param string $dst
303
- * @param int $buffersize
304
- * @return boolean
305
- */
306
- private function copyBig( $src, $dst, $buffersize ) {
307
- $src = fopen( $src, 'r' );
308
- $dest = fopen( $dst, 'w' );
309
-
310
- // Try first method:
311
- while ( !feof( $src ) ) {
312
- if( false === fwrite( $dest, fread( $src, $buffersize ) ) ) {
313
- $error = true;
314
- }
315
- }
316
- // Try second method if first one failed
317
- if( isset( $error ) && ($error === true) ) {
318
- while ( !feof( $src ) ) {
319
- if( false === stream_copy_to_stream( $src, $dest, 1024 ) ) {
320
- $this->log( "Can not copy file; {$src} -> {$dest}" );
321
- fclose( $src );
322
- fclose( $dest );
323
- return false;
324
- }
325
- }
326
- }
327
- // Close any open handler
328
- fclose( $src );
329
- fclose( $dest );
330
- return true;
331
- }
332
-
333
- /**
334
- * Check if certain file is excluded from copying process
335
- *
336
- * @param string $file filename including ending without full path
337
- * @return boolean
338
- */
339
- private function isFileExcluded( $file ) {
340
- // If file name exists
341
- if( in_array( basename( $file ), $this->options->excludedFiles ) ) {
342
- return true;
343
- }
344
-
345
- // Do not copy wp-config.php if the clone gets updated. This is for security purposes,
346
- // because if the updating process fails, the staging site would not be accessable any longer
347
- if( isset( $this->options->mainJob ) && $this->options->mainJob == "updating" && stripos( strrev( $file ), strrev( "wp-config.php" ) ) === 0 ) {
348
- return true;
349
- }
350
-
351
-
352
- return false;
353
- }
354
-
355
- /**
356
- * Check if certain file is excluded from copying process
357
- *
358
- * @param string $file filename including ending + (part) path e.g wp-content/db.php
359
- * @return boolean
360
- */
361
- private function isFileExcludedFullPath( $file ) {
362
- // If path + file exists
363
- foreach ( $this->options->excludedFilesFullPath as $excludedFile ) {
364
- if( false !== strpos( $file, $excludedFile ) ) {
365
- return true;
366
- }
367
- }
368
-
369
- return false;
370
- }
371
-
372
- /**
373
- * Replace backward slash with forward slash directory separator
374
- * Escape Windows Backward Slash - Compatibility Fix
375
-
376
- * @param string $path Path
377
- *
378
- * @return string
379
- */
380
- private function sanitizeDirectorySeparator( $path ) {
381
- //$string = str_replace( '\\', '/', $path );
382
- //$string = str_replace( "/", "\\", $path );
383
- //return str_replace( '\\\\', '\\', $string );
384
- //return preg_replace( '/[\\\\]+/', '\\\\\\\\', $string );
385
- return preg_replace( '/[\\\\]+/', '/', $path );
386
- }
387
-
388
- /**
389
- * Check if directory is excluded from copying
390
- * @param string $directory
391
- * @return bool
392
- */
393
- private function isDirectoryExcluded( $directory ) {
394
- // Make sure that wp-staging-pro directory / plugin is never excluded
395
- if( false !== strpos( $directory, 'wp-staging' ) || false !== strpos( $directory, 'wp-staging-pro' ) ) {
396
- return false;
397
- }
398
- // $directory = wpstg_replace_windows_directory_separator( $directory );
399
- // $directory = trailingslashit( $directory );
400
- $directory = trailingslashit( $this->sanitizeDirectorySeparator( $directory ) );
401
-
402
- foreach ( $this->options->excludedDirectories as $excludedDirectory ) {
403
- // $excludedDirectory = wpstg_replace_windows_directory_separator( $excludedDirectory );
404
- // $excludedDirectory = trailingslashit( $excludedDirectory );
405
- $excludedDirectory = trailingslashit( $this->sanitizeDirectorySeparator( $excludedDirectory ) );
406
- if( strpos( $directory, $excludedDirectory ) === 0 && !$this->isExtraDirectory( $directory ) ) {
407
- return true;
408
- }
409
- }
410
-
411
- return false;
412
- }
413
-
414
- /**
415
- * Check if directory is an extra directory and should be copied
416
- * @param string $directory
417
- * @return boolean
418
- */
419
- private function isExtraDirectory( $directory ) {
420
- $directory = $this->sanitizeDirectorySeparator( $directory );
421
-
422
- foreach ( $this->options->extraDirectories as $extraDirectory ) {
423
- if( strpos( $directory, $this->sanitizeDirectorySeparator( $extraDirectory ) ) === 0 ) {
424
- return true;
425
- }
426
- }
427
-
428
- return false;
429
- }
430
-
431
- }
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
+ // if( false !== strpos( $file, 'index.php' ) ) {
139
+ // $test = $file;
140
+ // }
141
+ $this->copyFile( $file );
142
+ }
143
+
144
+
145
+
146
+ $totalFiles = $this->options->copiedFiles;
147
+ // Log this only every 50 entries to keep the log small and to not block the rendering browser
148
+ if( $this->options->copiedFiles % 50 == 0 ) {
149
+ $this->log( "Total {$totalFiles} files processed" );
150
+ }
151
+
152
+ return true;
153
+ }
154
+
155
+ /**
156
+ * Checks Whether There is Any Job to Execute or Not
157
+ * @return bool
158
+ */
159
+ private function isFinished() {
160
+ return (
161
+ $this->options->currentStep > $this->options->totalSteps ||
162
+ $this->options->copiedFiles >= $this->options->totalFiles
163
+ );
164
+ }
165
+
166
+ /**
167
+ * @param string $file
168
+ * @return bool
169
+ */
170
+ private function copyFile( $file ) {
171
+
172
+ $file = trim( \WPStaging\WPStaging::getWPpath() . $file );
173
+
174
+ $directory = dirname( $file );
175
+
176
+ // Directory is excluded
177
+ if( $this->isDirectoryExcluded( $directory ) ) {
178
+ $this->debugLog( "Skipping directory by rule: {$file}", Logger::TYPE_INFO );
179
+ return false;
180
+ }
181
+
182
+ // File is excluded
183
+ if( $this->isFileExcluded( $file ) ) {
184
+ $this->debugLog( "Skipping file by rule: {$file}", Logger::TYPE_INFO );
185
+ return false;
186
+ }
187
+ // Path + File is excluded
188
+ if( $this->isFileExcludedFullPath( $file ) ) {
189
+ $this->debugLog( "Skipping file by rule: {$file}", Logger::TYPE_INFO );
190
+ return false;
191
+ }
192
+
193
+ // Invalid file, skipping it as if succeeded
194
+ if( !is_file( $file ) ) {
195
+ $this->log( "File doesn't exist {$file}", Logger::TYPE_WARNING );
196
+ return true;
197
+ }
198
+ // Invalid file, skipping it as if succeeded
199
+ if( !is_readable( $file ) ) {
200
+ $this->log( "Can't read file {$file}", Logger::TYPE_WARNING );
201
+ return true;
202
+ }
203
+
204
+
205
+ // Get file size
206
+ $fileSize = filesize( $file );
207
+
208
+ // File is over maximum allowed file size (8MB)
209
+ if( $fileSize >= $this->settings->maxFileSize * 1000000 ) {
210
+ $this->log( "Skipping big file: {$file}", Logger::TYPE_INFO );
211
+ return false;
212
+ }
213
+
214
+ // Failed to get destination
215
+ if( false === ($destination = $this->getDestination( $file )) ) {
216
+ $this->log( "Can't get the destination of {$file}", Logger::TYPE_WARNING );
217
+ return false;
218
+ }
219
+
220
+ // File is over batch size
221
+ if( $fileSize >= $this->settings->batchSize ) {
222
+ $this->log( "Trying to copy big file: {$file} -> {$destination}", Logger::TYPE_INFO );
223
+ return $this->copyBig( $file, $destination, $this->settings->batchSize );
224
+ }
225
+
226
+ // Attempt to copy
227
+ if( !@copy( $file, $destination ) ) {
228
+ $errors = error_get_last();
229
+ $this->log( "Files: Failed to copy file to destination. Error: {$errors['message']} {$file} -> {$destination}", Logger::TYPE_ERROR );
230
+ return false;
231
+ }
232
+
233
+ // Set file permissions
234
+ @chmod( $file, wpstg_get_permissions_for_file() );
235
+
236
+ $this->setDirPermissions( $file );
237
+
238
+ return true;
239
+ }
240
+
241
+ /**
242
+ * Set directory permissions
243
+ * @param type $file
244
+ * @return boolean
245
+ */
246
+ private function setDirPermissions( $file ) {
247
+ $dir = dirname( $file );
248
+ if( is_dir( $dir ) ) {
249
+ @chmod( $dir, wpstg_get_permissions_for_directory() );
250
+ }
251
+ return false;
252
+ }
253
+
254
+ /**
255
+ * Gets destination file and checks if the directory exists, if it does not attempts to create it.
256
+ * If creating destination directory fails, it returns false, gives destination full path otherwise
257
+ * @param string $file
258
+ * @return bool|string
259
+ */
260
+ private function getDestination( $file ) {
261
+ $file = $this->getMultisiteUploadFolder( $file );
262
+ $relativePath = str_replace( \WPStaging\WPStaging::getWPpath(), null, $file );
263
+ $destinationPath = $this->destination . $relativePath;
264
+ $destinationDirectory = dirname( $destinationPath );
265
+
266
+ if( !is_dir( $destinationDirectory ) && !@mkdir( $destinationDirectory, wpstg_get_permissions_for_directory(), true ) ) {
267
+ $this->log( "Files: Can not create directory {$destinationDirectory}", Logger::TYPE_ERROR );
268
+ return false;
269
+ }
270
+
271
+ return $this->sanitizeDirectorySeparator( $destinationPath );
272
+ }
273
+
274
+ /**
275
+ * Replace relative path of file if its located in multisite upload folder
276
+ * wp-content/uploads/sites/SITEID or old wordpress structure wp-content/blogs.dir/SITEID/files
277
+ * @return boolean
278
+ */
279
+ private function getMultisiteUploadFolder( $file ) {
280
+ // Check first which method is used
281
+ $uploads = wp_upload_dir();
282
+ $basedir = $uploads['basedir'];
283
+
284
+ if( false === strpos( $basedir, 'blogs.dir' ) ) {
285
+ // Since WP 3.5
286
+ $search = 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'sites' . DIRECTORY_SEPARATOR . get_current_blog_id();
287
+ $replace = 'wp-content' . DIRECTORY_SEPARATOR . 'uploads';
288
+ $uploadsFolder = str_replace( $search, $replace, $file );
289
+ } else {
290
+ // old blog structure
291
+ $search = 'wp-content' . DIRECTORY_SEPARATOR . 'blogs.dir' . DIRECTORY_SEPARATOR . get_current_blog_id() . DIRECTORY_SEPARATOR . 'files';
292
+ $replace = 'wp-content' . DIRECTORY_SEPARATOR . 'uploads';
293
+ $uploadsFolder = str_replace( $search, $replace, $file );
294
+ }
295
+
296
+ return $uploadsFolder;
297
+ }
298
+
299
+ /**
300
+ * Copy bigger files than $this->settings->batchSize
301
+ * @param string $src
302
+ * @param string $dst
303
+ * @param int $buffersize
304
+ * @return boolean
305
+ */
306
+ private function copyBig( $src, $dst, $buffersize ) {
307
+ $src = fopen( $src, 'r' );
308
+ $dest = fopen( $dst, 'w' );
309
+
310
+ // Try first method:
311
+ while ( !feof( $src ) ) {
312
+ if( false === fwrite( $dest, fread( $src, $buffersize ) ) ) {
313
+ $error = true;
314
+ }
315
+ }
316
+ // Try second method if first one failed
317
+ if( isset( $error ) && ($error === true) ) {
318
+ while ( !feof( $src ) ) {
319
+ if( false === stream_copy_to_stream( $src, $dest, 1024 ) ) {
320
+ $this->log( "Can not copy file; {$src} -> {$dest}" );
321
+ fclose( $src );
322
+ fclose( $dest );
323
+ return false;
324
+ }
325
+ }
326
+ }
327
+ // Close any open handler
328
+ fclose( $src );
329
+ fclose( $dest );
330
+ return true;
331
+ }
332
+
333
+ /**
334
+ * Check if certain file is excluded from copying process
335
+ *
336
+ * @param string $file filename including ending without full path
337
+ * @return boolean
338
+ */
339
+ private function isFileExcluded( $file ) {
340
+ // If file name exists
341
+ if( in_array( basename( $file ), $this->options->excludedFiles ) ) {
342
+ return true;
343
+ }
344
+
345
+ // Do not copy wp-config.php if the clone gets updated. This is for security purposes,
346
+ // because if the updating process fails, the staging site would not be accessable any longer
347
+ if( isset( $this->options->mainJob ) && $this->options->mainJob == "updating" && stripos( strrev( $file ), strrev( "wp-config.php" ) ) === 0 ) {
348
+ return true;
349
+ }
350
+
351
+
352
+ return false;
353
+ }
354
+
355
+ /**
356
+ * Check if certain file is excluded from copying process
357
+ *
358
+ * @param string $file filename including ending + (part) path e.g wp-content/db.php
359
+ * @return boolean
360
+ */
361
+ private function isFileExcludedFullPath( $file ) {
362
+ // If path + file exists
363
+ foreach ( $this->options->excludedFilesFullPath as $excludedFile ) {
364
+ if( false !== strpos( $file, $excludedFile ) ) {
365
+ return true;
366
+ }
367
+ }
368
+
369
+ return false;
370
+ }
371
+
372
+ /**
373
+ * Replace backward slash with forward slash directory separator
374
+ * Escape Windows Backward Slash - Compatibility Fix
375
+
376
+ * @param string $path Path
377
+ *
378
+ * @return string
379
+ */
380
+ private function sanitizeDirectorySeparator( $path ) {
381
+ //$string = str_replace( '\\', '/', $path );
382
+ //$string = str_replace( "/", "\\", $path );
383
+ //return str_replace( '\\\\', '\\', $string );
384
+ //return preg_replace( '/[\\\\]+/', '\\\\\\\\', $string );
385
+ return preg_replace( '/[\\\\]+/', '/', $path );
386
+ }
387
+
388
+ /**
389
+ * Check if directory is excluded from copying
390
+ * @param string $directory
391
+ * @return bool
392
+ */
393
+ private function isDirectoryExcluded( $directory ) {
394
+ // Make sure that wp-staging-pro directory / plugin is never excluded
395
+ if( false !== strpos( $directory, 'wp-staging' ) || false !== strpos( $directory, 'wp-staging-pro' ) ) {
396
+ return false;
397
+ }
398
+ // $directory = wpstg_replace_windows_directory_separator( $directory );
399
+ // $directory = trailingslashit( $directory );
400
+ $directory = trailingslashit( $this->sanitizeDirectorySeparator( $directory ) );
401
+
402
+ foreach ( $this->options->excludedDirectories as $excludedDirectory ) {
403
+ // $excludedDirectory = wpstg_replace_windows_directory_separator( $excludedDirectory );
404
+ // $excludedDirectory = trailingslashit( $excludedDirectory );
405
+ $excludedDirectory = trailingslashit( $this->sanitizeDirectorySeparator( $excludedDirectory ) );
406
+ if( strpos( $directory, $excludedDirectory ) === 0 && !$this->isExtraDirectory( $directory ) ) {
407
+ return true;
408
+ }
409
+ }
410
+
411
+ return false;
412
+ }
413
+
414
+ /**
415
+ * Check if directory is an extra directory and should be copied
416
+ * @param string $directory
417
+ * @return boolean
418
+ */
419
+ private function isExtraDirectory( $directory ) {
420
+ $directory = $this->sanitizeDirectorySeparator( $directory );
421
+
422
+ foreach ( $this->options->extraDirectories as $extraDirectory ) {
423
+ if( strpos( $directory, $this->sanitizeDirectorySeparator( $extraDirectory ) ) === 0 ) {
424
+ return true;
425
+ }
426
+ }
427
+
428
+ return false;
429
+ }
430
+
431
+ }
apps/Backend/Modules/Jobs/Multisite/Finish.php CHANGED
@@ -1,163 +1,163 @@
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
- $multisite = new Multisite;
36
-
37
-
38
-
39
- $return = array(
40
- "directoryName" => $this->options->cloneDirectoryName,
41
- "path" => $this->options->destinationDir,
42
- "url" => $this->getDestinationUrl(),
43
- "number" => $this->options->cloneNumber,
44
- "version" => \WPStaging\WPStaging::VERSION,
45
- "status" => 'finished',
46
- "prefix" => $this->options->prefix,
47
- "last_msg" => $this->logger->getLastLogMsg(),
48
- "job" => $this->options->currentJob,
49
- "percentage" => 100
50
- );
51
-
52
- //$this->flush();
53
-
54
- return ( object ) $return;
55
- }
56
-
57
- /**
58
- * Delete Cache Files
59
- */
60
- protected function deleteCacheFiles() {
61
- $this->log( "Finish: Deleting clone job's cache files..." );
62
-
63
- // Clean cache files
64
- $this->cache->delete( "clone_options" );
65
- $this->cache->delete( "files_to_copy" );
66
-
67
- $this->log( "Finish: Clone job's cache files have been deleted!" );
68
- }
69
-
70
- /**
71
- * Prepare clone records
72
- * @return bool
73
- */
74
- protected function prepareCloneDataRecords() {
75
- // Check if clones still exist
76
- $this->log( "Finish: Verifying existing clones..." );
77
-
78
- // Clone data already exists
79
- if( isset( $this->options->existingClones[$this->options->clone] ) ) {
80
- $this->options->existingClones[$this->options->clone]['datetime'] = time();
81
- $this->options->existingClones[$this->options->clone]['url'] = $this->getDestinationUrl();
82
- update_option( "wpstg_existing_clones_beta", $this->options->existingClones );
83
- $this->log( "Finish: The job finished!" );
84
- return true;
85
- }
86
-
87
- // Save new clone data
88
- $this->log( "Finish: {$this->options->clone}'s clone job's data is not in database, generating data" );
89
-
90
- // sanitize the clone name before saving
91
- //$clone = preg_replace("#\W+#", '-', strtolower($this->options->clone));
92
-
93
- $this->options->existingClones[$this->clone] = array(
94
- "directoryName" => $this->options->cloneDirectoryName,
95
- "path" => $this->options->destinationDir,
96
- "url" => $this->getDestinationUrl(),
97
- "number" => $this->options->cloneNumber,
98
- "version" => \WPStaging\WPStaging::VERSION,
99
- "status" => false,
100
- "prefix" => $this->options->prefix,
101
- "datetime" => time(),
102
- "databaseUser" => $this->options->databaseUser,
103
- "databasePassword" => $this->options->databasePassword,
104
- "databaseDatabase" => $this->options->databaseDatabase,
105
- "databaseServer" => $this->options->databaseServer,
106
- "databasePrefix" => $this->options->databasePrefix,
107
- );
108
-
109
- if( false === update_option( "wpstg_existing_clones_beta", $this->options->existingClones ) ) {
110
- $this->log( "Finish: Failed to save {$this->options->clone}'s clone job data to database'" );
111
- return false;
112
- }
113
-
114
- return true;
115
- }
116
-
117
-
118
- /**
119
- * Get destination Hostname depending on wheather WP has been installed in sub dir or not
120
- * @return type
121
- */
122
- private function getDestinationUrl() {
123
-
124
- if( !empty( $this->options->cloneHostname ) ) {
125
- return $this->options->cloneHostname;
126
- }
127
-
128
- return trailingslashit( $this->multisiteHomeDomain ) . $this->options->cloneDirectoryName;
129
- }
130
-
131
- /**
132
- * Check if WP is installed in subdir
133
- * @return boolean
134
- */
135
- // private function isSubDir() {
136
- // // Compare names without scheme to bypass cases where siteurl and home have different schemes http / https
137
- // // This is happening much more often than you would expect
138
- // $siteurl = preg_replace( '#^https?://#', '', rtrim( get_option( 'siteurl' ), '/' ) );
139
- // $home = preg_replace( '#^https?://#', '', rtrim( get_option( 'home' ), '/' ) );
140
- //
141
- // if( $home !== $siteurl ) {
142
- // return true;
143
- // }
144
- // return false;
145
- // }
146
-
147
- /**
148
- * Get the install sub directory if WP is installed in sub directory
149
- * @return string
150
- */
151
- // private function getSubDir() {
152
- // $home = get_option( 'home' );
153
- // $siteurl = get_option( 'siteurl' );
154
- //
155
- // if( empty( $home ) || empty( $siteurl ) ) {
156
- // return '';
157
- // }
158
- //
159
- // $dir = str_replace( $home, '', $siteurl );
160
- // return str_replace( '/', '', $dir );
161
- // }
162
-
163
- }
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
+ $multisite = new Multisite;
36
+
37
+
38
+
39
+ $return = array(
40
+ "directoryName" => $this->options->cloneDirectoryName,
41
+ "path" => $this->options->destinationDir,
42
+ "url" => $this->getDestinationUrl(),
43
+ "number" => $this->options->cloneNumber,
44
+ "version" => \WPStaging\WPStaging::VERSION,
45
+ "status" => 'finished',
46
+ "prefix" => $this->options->prefix,
47
+ "last_msg" => $this->logger->getLastLogMsg(),
48
+ "job" => $this->options->currentJob,
49
+ "percentage" => 100
50
+ );
51
+
52
+ //$this->flush();
53
+
54
+ return ( object ) $return;
55
+ }
56
+
57
+ /**
58
+ * Delete Cache Files
59
+ */
60
+ protected function deleteCacheFiles() {
61
+ $this->log( "Finish: Deleting clone job's cache files..." );
62
+
63
+ // Clean cache files
64
+ $this->cache->delete( "clone_options" );
65
+ $this->cache->delete( "files_to_copy" );
66
+
67
+ $this->log( "Finish: Clone job's cache files have been deleted!" );
68
+ }
69
+
70
+ /**
71
+ * Prepare clone records
72
+ * @return bool
73
+ */
74
+ protected function prepareCloneDataRecords() {
75
+ // Check if clones still exist
76
+ $this->log( "Finish: Verifying existing clones..." );
77
+
78
+ // Clone data already exists
79
+ if( isset( $this->options->existingClones[$this->options->clone] ) ) {
80
+ $this->options->existingClones[$this->options->clone]['datetime'] = time();
81
+ $this->options->existingClones[$this->options->clone]['url'] = $this->getDestinationUrl();
82
+ update_option( "wpstg_existing_clones_beta", $this->options->existingClones );
83
+ $this->log( "Finish: The job finished!" );
84
+ return true;
85
+ }
86
+
87
+ // Save new clone data
88
+ $this->log( "Finish: {$this->options->clone}'s clone job's data is not in database, generating data" );
89
+
90
+ // sanitize the clone name before saving
91
+ //$clone = preg_replace("#\W+#", '-', strtolower($this->options->clone));
92
+
93
+ $this->options->existingClones[$this->clone] = array(
94
+ "directoryName" => $this->options->cloneDirectoryName,
95
+ "path" => $this->options->destinationDir,
96
+ "url" => $this->getDestinationUrl(),
97
+ "number" => $this->options->cloneNumber,
98
+ "version" => \WPStaging\WPStaging::VERSION,
99
+ "status" => false,
100
+ "prefix" => $this->options->prefix,
101
+ "datetime" => time(),
102
+ "databaseUser" => $this->options->databaseUser,
103
+ "databasePassword" => $this->options->databasePassword,
104
+ "databaseDatabase" => $this->options->databaseDatabase,
105
+ "databaseServer" => $this->options->databaseServer,
106
+ "databasePrefix" => $this->options->databasePrefix,
107
+ );
108
+
109
+ if( false === update_option( "wpstg_existing_clones_beta", $this->options->existingClones ) ) {
110
+ $this->log( "Finish: Failed to save {$this->options->clone}'s clone job data to database'" );
111
+ return false;
112
+ }
113
+
114
+ return true;
115
+ }
116
+
117
+
118
+ /**
119
+ * Get destination Hostname depending on wheather WP has been installed in sub dir or not
120
+ * @return type
121
+ */
122
+ private function getDestinationUrl() {
123
+
124
+ if( !empty( $this->options->cloneHostname ) ) {
125
+ return $this->options->cloneHostname;
126
+ }
127
+
128
+ return trailingslashit( $this->multisiteHomeDomain ) . $this->options->cloneDirectoryName;
129
+ }
130
+
131
+ /**
132
+ * Check if WP is installed in subdir
133
+ * @return boolean
134
+ */
135
+ // private function isSubDir() {
136
+ // // Compare names without scheme to bypass cases where siteurl and home have different schemes http / https
137
+ // // This is happening much more often than you would expect
138
+ // $siteurl = preg_replace( '#^https?://#', '', rtrim( get_option( 'siteurl' ), '/' ) );
139
+ // $home = preg_replace( '#^https?://#', '', rtrim( get_option( 'home' ), '/' ) );
140
+ //
141
+ // if( $home !== $siteurl ) {
142
+ // return true;
143
+ // }
144
+ // return false;
145
+ // }
146
+
147
+ /**
148
+ * Get the install sub directory if WP is installed in sub directory
149
+ * @return string
150
+ */
151
+ // private function getSubDir() {
152
+ // $home = get_option( 'home' );
153
+ // $siteurl = get_option( 'siteurl' );
154
+ //
155
+ // if( empty( $home ) || empty( $siteurl ) ) {
156
+ // return '';
157
+ // }
158
+ //
159
+ // $dir = str_replace( $home, '', $siteurl );
160
+ // return str_replace( '/', '', $dir );
161
+ // }
162
+
163
+ }
apps/Backend/Modules/Jobs/Multisite/SearchReplace.php CHANGED
@@ -1,819 +1,819 @@
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
- return trailingslashit( $this->strings->getUrlWithoutScheme($this->multisiteDomainWithoutScheme) ) . $this->options->cloneDirectoryName;
200
- }
201
-
202
- /**
203
- * Get the install sub directory if WP is installed in sub directory
204
- * @return string
205
- */
206
- private function getSubDir() {
207
- $home = get_option( 'home' );
208
- $siteurl = get_option( 'siteurl' );
209
-
210
- if( empty( $home ) || empty( $siteurl ) ) {
211
- return '';
212
- }
213
-
214
- $dir = str_replace( $home, '', $siteurl );
215
- return str_replace( '/', '', $dir );
216
- }
217
-
218
- /**
219
- * Start search replace job
220
- * @param string $new
221
- * @param string $old
222
- */
223
- private function startReplace( $table ) {
224
- $rows = $this->options->job->start + $this->settings->querySRLimit;
225
- $this->log(
226
- "DB Processing: Table {$table} {$this->options->job->start} to {$rows} records"
227
- );
228
-
229
- // Search & Replace
230
- $this->searchReplace( $table, $rows, array() );
231
-
232
- // Set new offset
233
- $this->options->job->start += $this->settings->querySRLimit;
234
- }
235
-
236
- /**
237
- * Returns the number of pages in a table.
238
- * @access public
239
- * @return int
240
- */
241
- private function get_pages_in_table( $table ) {
242
-
243
- // Table does not exists
244
- $result = $this->db->query( "SHOW TABLES LIKE '{$table}'" );
245
- if( !$result || 0 === $result ) {
246
- return 0;
247
- }
248
-
249
- $table = esc_sql( $table );
250
- $rows = $this->db->get_var( "SELECT COUNT(*) FROM $table" );
251
- $pages = ceil( $rows / $this->settings->querySRLimit );
252
- return absint( $pages );
253
- }
254
-
255
- /**
256
- * Gets the columns in a table.
257
- * @access public
258
- * @param string $table The table to check.
259
- * @return array
260
- */
261
- private function get_columns( $table ) {
262
- $primary_key = null;
263
- $columns = array();
264
- $fields = $this->db->get_results( 'DESCRIBE ' . $table );
265
- if( is_array( $fields ) ) {
266
- foreach ( $fields as $column ) {
267
- $columns[] = $column->Field;
268
- if( $column->Key == 'PRI' ) {
269
- $primary_key = $column->Field;
270
- }
271
- }
272
- }
273
- return array($primary_key, $columns);
274
- }
275
-
276
- /**
277
- * Return absolute destination path
278
- * @return string
279
- */
280
- private function getAbsDestination() {
281
- if( empty( $this->options->cloneDir ) ) {
282
- return \WPStaging\WPStaging::getWPpath();
283
- }
284
- return trailingslashit( $this->options->cloneDir );
285
- }
286
-
287
- /**
288
- * Adapated from interconnect/it's search/replace script, adapted from Better Search Replace
289
- *
290
- * Modified to use WordPress wpdb functions instead of PHP's native mysql/pdo functions,
291
- * and to be compatible with batch processing.
292
- *
293
- * @link https://interconnectit.com/products/search-and-replace-for-wordpress-databases/
294
- *
295
- * @access public
296
- * @param string $table The table to run the replacement on.
297
- * @param int $page The page/block to begin the query on.
298
- * @param array $args An associative array containing arguments for this run.
299
- * @return array
300
- */
301
- private function searchReplace( $table, $page, $args ) {
302
-
303
- if( $this->thirdParty->isSearchReplaceExcluded( $table ) ) {
304
- $this->log( "DB Processing: Skip {$table}", \WPStaging\Utils\Logger::TYPE_INFO );
305
- return true;
306
- }
307
-
308
- // Load up the default settings for this chunk.
309
- $table = esc_sql( $table );
310
- $current_page = $this->options->job->start + $this->settings->querySRLimit;
311
- $pages = $this->get_pages_in_table( $table );
312
-
313
-
314
- // Search URL example.com/staging and root path to staging site /var/www/htdocs/staging
315
- $args['search_for'] = array(
316
- '//' . $this->getSourceHostname(),
317
- ABSPATH,
318
- str_replace( '/', '\/', $this->getSourceHostname() ), // // Used by revslider and several visual editors
319
- $this->getImagePathLive()
320
- );
321
-
322
-
323
- $args['replace_with'] = array(
324
- '//' . $this->getDestinationHostname(),
325
- $this->options->destinationDir,
326
- str_replace( '/', '\/', $this->getDestinationHostname() ), // Used by revslider and several visual editors
327
- $this->getImagePathStaging()
328
- );
329
-
330
-
331
- ##########################
332
- // if( $this->isSubDir() ) {
333
- // // Search URL example.com/staging and root path to staging site /var/www/htdocs/staging
334
- // $args['search_for'] = array(
335
- // rtrim( $this->multisiteHomeUrlWithoutScheme, "/" ) . $this->getSubDir(),
336
- // ABSPATH,
337
- // str_replace( '/', '\/', rtrim( $this->multisiteHomeUrlWithoutScheme, '/' ) ) . str_replace( '/', '\/', $this->getSubDir() ), // // Used by revslider and several visual editors
338
- // $this->getImagePathLive()
339
- // );
340
- //
341
- //
342
- // $args['replace_with'] = array(
343
- // rtrim( $this->multisiteDomainWithoutScheme, "/" ) . $this->getSubDir() . '/' . $this->options->cloneDirectoryName,
344
- // rtrim( $this->getAbsDestination(), '/' ) . '/' . $this->options->cloneDirectoryName,
345
- // str_replace( '/', '\/', rtrim( $this->multisiteDomainWithoutScheme, "/" ) ) . str_replace( '/', '\/', $this->getSubDir() ) . '\/' . $this->options->cloneDirectoryName, // Used by revslider and several visual editors
346
- // $this->getImagePathStaging()
347
- // );
348
- // } else {
349
- // $args['search_for'] = array(
350
- // rtrim( $this->multisiteHomeUrlWithoutScheme, '/' ),
351
- // ABSPATH,
352
- // str_replace( '/', '\/', rtrim( $this->multisiteHomeUrlWithoutScheme, '/' ) ),
353
- // $this->getImagePathLive()
354
- // );
355
- // $args['replace_with'] = array(
356
- // rtrim( $this->multisiteDomainWithoutScheme, '/' ) . '/' . $this->options->cloneDirectoryName,
357
- // rtrim( $this->getAbsDestination(), '/' ) . '/' . $this->options->cloneDirectoryName,
358
- // str_replace( '/', '\/', rtrim( $this->multisiteDomainWithoutScheme, '/' ) ) . '\/' . $this->options->cloneDirectoryName,
359
- // $this->getImagePathStaging()
360
- // );
361
- // }
362
-
363
-
364
-
365
-
366
- $args['replace_guids'] = 'off';
367
- $args['dry_run'] = 'off';
368
- $args['case_insensitive'] = false;
369
- $args['replace_mails'] = 'off';
370
- $args['skip_transients'] = 'on';
371
-
372
-
373
- // Allow filtering of search & replace parameters
374
- $args = apply_filters( 'wpstg_clone_searchreplace_params', $args );
375
-
376
- // Get a list of columns in this table.
377
- list( $primary_key, $columns ) = $this->get_columns( $table );
378
-
379
- // Bail out early if there isn't a primary key.
380
- // We commented this to search & replace through tables which have no primary keys like wp_revslider_slides
381
- // @todo test this carefully. If it causes (performance) issues we need to activate it again!
382
- // @since 2.4.4
383
- // if( null === $primary_key ) {
384
- // return false;
385
- // }
386
-
387
- $current_row = 0;
388
- $start = $this->options->job->start;
389
- $end = $this->settings->querySRLimit;
390
-
391
- // Grab the content of the table.
392
- $data = $this->db->get_results( "SELECT * FROM $table LIMIT $start, $end", ARRAY_A );
393
-
394
- // Filter certain rows option_name in wpstg_options
395
- $filter = array(
396
- 'Admin_custome_login_Slidshow',
397
- 'Admin_custome_login_Social',
398
- 'Admin_custome_login_logo',
399
- 'Admin_custome_login_text',
400
- 'Admin_custome_login_login',
401
- 'Admin_custome_login_top',
402
- 'Admin_custome_login_dashboard',
403
- 'Admin_custome_login_Version',
404
- 'upload_path',
405
- );
406
-
407
- $filter = apply_filters( 'wpstg_clone_searchreplace_excl_rows', $filter );
408
-
409
- // Loop through the data.
410
- foreach ( $data as $row ) {
411
- $current_row++;
412
- $update_sql = array();
413
- $where_sql = array();
414
- $upd = false;
415
-
416
- // Skip rows below
417
- if( isset( $row['option_name'] ) && in_array( $row['option_name'], $filter ) ) {
418
- continue;
419
- }
420
-
421
- // Skip rows with transients (They can store huge data and we need to save memory)
422
- if( isset( $row['option_name'] ) && 'on' === $args['skip_transients'] && false !== strpos( $row['option_name'], '_transient' ) ) {
423
- continue;
424
- }
425
-
426
- foreach ( $columns as $column ) {
427
-
428
- $dataRow = $row[$column];
429
-
430
- if( $column == $primary_key ) {
431
- $where_sql[] = $column . ' = "' . $this->mysql_escape_mimic( $dataRow ) . '"';
432
- continue;
433
- }
434
-
435
- // Skip GUIDs by default.
436
- if( 'on' !== $args['replace_guids'] && 'guid' == $column ) {
437
- continue;
438
- }
439
-
440
- // Skip mail addresses
441
- if( 'off' === $args['replace_mails'] && false !== strpos( $dataRow, '@' . $this->multisiteDomainWithoutScheme ) ) {
442
- continue;
443
- }
444
-
445
- // Check options table
446
- if( $this->options->prefix . 'options' === $table ) {
447
-
448
- // Skip certain options
449
- if( isset( $should_skip ) && true === $should_skip ) {
450
- $should_skip = false;
451
- continue;
452
- }
453
-
454
- // Skip this row
455
- if( 'wpstg_existing_clones_beta' === $dataRow ||
456
- 'wpstg_existing_clones' === $dataRow ||
457
- 'wpstg_settings' === $dataRow ||
458
- 'wpstg_license_status' === $dataRow ||
459
- 'siteurl' === $dataRow ||
460
- 'home' === $dataRow
461
- ) {
462
- $should_skip = true;
463
- }
464
- }
465
-
466
- // Check the path delimiter for / or \/ and remove one of those which prevents from resulting in wrong syntax like domain.com/staging\/.
467
- // 1. local.wordpress.test -> local.wordpress.test/staging
468
- // 2. local.wordpress.test\/ -> local.wordpress.test\/staging\/
469
- $tmp = $args;
470
- if( false === strpos( $dataRow, $tmp['search_for'][0] ) ) {
471
- array_shift( $tmp['search_for'] ); // rtrim( $this->homeUrl, '/' ),
472
- array_shift( $tmp['replace_with'] ); // rtrim( $this->homeUrl, '/' ) . '/' . $this->options->cloneDirectoryName,
473
- } else {
474
- unset( $tmp['search_for'][1] );
475
- unset( $tmp['replace_with'][1] );
476
- // recount array
477
- $tmp['search_for'] = array_values( $tmp['search_for'] );
478
- $tmp['replace_with'] = array_values( $tmp['replace_with'] );
479
- }
480
-
481
- // Run a search replace on the data row and respect the serialisation.
482
- $i = 0;
483
- foreach ( $tmp['search_for'] as $replace ) {
484
- $dataRow = $this->recursive_unserialize_replace( $tmp['search_for'][$i], $tmp['replace_with'][$i], $dataRow, false, $args['case_insensitive'] );
485
- $i++;
486
- }
487
- unset( $replace );
488
- unset( $i );
489
- unset( $tmp );
490
-
491
- // Something was changed
492
- if( $row[$column] != $dataRow ) {
493
- $update_sql[] = $column . ' = "' . $this->mysql_escape_mimic( $dataRow ) . '"';
494
- $upd = true;
495
- }
496
- }
497
-
498
- // Determine what to do with updates.
499
- if( $args['dry_run'] === 'on' ) {
500
- // Don't do anything if a dry run
501
- } elseif( $upd && !empty( $where_sql ) ) {
502
- // If there are changes to make, run the query.
503
- $sql = 'UPDATE ' . $table . ' SET ' . implode( ', ', $update_sql ) . ' WHERE ' . implode( ' AND ', array_filter( $where_sql ) );
504
- $result = $this->db->query( $sql );
505
-
506
- if( !$result ) {
507
- $this->log( "Error updating row {$current_row} SQL: {$sql}", \WPStaging\Utils\Logger::TYPE_ERROR );
508
- }
509
- }
510
- } // end row loop
511
- unset( $row );
512
- unset( $update_sql );
513
- unset( $where_sql );
514
- unset( $sql );
515
-
516
-
517
- // DB Flush
518
- $this->db->flush();
519
- return true;
520
- }
521
-
522
- /**
523
- * Get path to multisite image folder e.g. wp-content/blogs.dir/ID/files or wp-content/uploads/sites/ID
524
- * @return string
525
- */
526
- private function getImagePathLive() {
527
- // Check first which structure is used
528
- $uploads = wp_upload_dir();
529
- $basedir = $uploads['basedir'];
530
- $blogId = get_current_blog_id();
531
-
532
- if( false === strpos( $basedir, 'blogs.dir' ) ) {
533
- // Since WP 3.5
534
- $path = $blogId > 1 ?
535
- 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'sites' . DIRECTORY_SEPARATOR . get_current_blog_id() . DIRECTORY_SEPARATOR :
536
- 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR;
537
- } else {
538
- // old blog structure
539
- $path = $blogId > 1 ?
540
- 'wp-content' . DIRECTORY_SEPARATOR . 'blogs.dir' . DIRECTORY_SEPARATOR . get_current_blog_id() . DIRECTORY_SEPARATOR . 'files' . DIRECTORY_SEPARATOR :
541
- 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR;
542
- }
543
- return $path;
544
- }
545
-
546
- /**
547
- * Get path to staging site image path wp-content/uploads
548
- * @return string
549
- */
550
- private function getImagePathStaging() {
551
- return 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR;
552
- }
553
-
554
- /**
555
- * Adapted from interconnect/it's search/replace script.
556
- *
557
- * @link https://interconnectit.com/products/search-and-replace-for-wordpress-databases/
558
- *
559
- * Take a serialised array and unserialise it replacing elements as needed and
560
- * unserialising any subordinate arrays and performing the replace on those too.
561
- *
562
- * @access private
563
- * @param string $from String we're looking to replace.
564
- * @param string $to What we want it to be replaced with
565
- * @param array $data Used to pass any subordinate arrays back to in.
566
- * @param boolean $serialized Does the array passed via $data need serialising.
567
- * @param sting|boolean $case_insensitive Set to 'on' if we should ignore case, false otherwise.
568
- *
569
- * @return string|array The original array with all elements replaced as needed.
570
- */
571
- private function recursive_unserialize_replace( $from = '', $to = '', $data = '', $serialized = false, $case_insensitive = false ) {
572
- try {
573
- // Some unserialized data cannot be re-serialized eg. SimpleXMLElements
574
- if( is_serialized( $data ) && ( $unserialized = @unserialize( $data ) ) !== false ) {
575
- $data = $this->recursive_unserialize_replace( $from, $to, $unserialized, true, $case_insensitive );
576
- } elseif( is_array( $data ) ) {
577
- $tmp = array();
578
- foreach ( $data as $key => $value ) {
579
- $tmp[$key] = $this->recursive_unserialize_replace( $from, $to, $value, false, $case_insensitive );
580
- }
581
-
582
- $data = $tmp;
583
- unset( $tmp );
584
- } elseif( is_object( $data ) ) {
585
- $tmp = $data;
586
- $props = get_object_vars( $data );
587
- foreach ( $props as $key => $value ) {
588
- if( $key === '' || ord( $key[0] ) === 0 ) {
589
- continue;
590
- }
591
- $tmp->$key = $this->recursive_unserialize_replace( $from, $to, $value, false, $case_insensitive );
592
- }
593
-
594
- $data = $tmp;
595
- unset( $tmp );
596
- } else {
597
- if( is_string( $data ) ) {
598
- if( !empty( $from ) && !empty( $to ) ) {
599
- $data = $this->str_replace( $from, $to, $data, $case_insensitive );
600
- }
601
- }
602
- }
603
-
604
- if( $serialized ) {
605
- return serialize( $data );
606
- }
607
- } catch ( Exception $error ) {
608
-
609
- }
610
-
611
- return $data;
612
- }
613
-
614
- /**
615
- * Check if the object is a valid one and not __PHP_Incomplete_Class_Name
616
- * Can not use is_object alone because in php 7.2 it's returning true even though object is __PHP_Incomplete_Class_Name
617
- * @return boolean
618
- */
619
- // private function isValidObject( $data ) {
620
- // if( !is_object( $data ) || gettype( $data ) != 'object' ) {
621
- // return false;
622
- // }
623
- //
624
- // $invalid_class_props = get_object_vars( $data );
625
- //
626
- // if (!isset($invalid_class_props['__PHP_Incomplete_Class_Name'])){
627
- // // Assume it must be an valid object
628
- // return true;
629
- // }
630
- //
631
- // $invalid_object_class = $invalid_class_props['__PHP_Incomplete_Class_Name'];
632
- //
633
- // if( !empty( $invalid_object_class ) ) {
634
- // return false;
635
- // }
636
- //
637
- // // Assume it must be an valid object
638
- // return true;
639
- // }
640
-
641
- /**
642
- * Mimics the mysql_real_escape_string function. Adapted from a post by 'feedr' on php.net.
643
- * @link http://php.net/manual/en/function.mysql-real-escape-string.php#101248
644
- * @access public
645
- * @param string $input The string to escape.
646
- * @return string
647
- */
648
- private function mysql_escape_mimic( $input ) {
649
- if( is_array( $input ) ) {
650
- return array_map( __METHOD__, $input );
651
- }
652
- if( !empty( $input ) && is_string( $input ) ) {
653
- return str_replace( array('\\', "\0", "\n", "\r", "'", '"', "\x1a"), array('\\\\', '\\0', '\\n', '\\r', "\\'", '\\"', '\\Z'), $input );
654
- }
655
-
656
- return $input;
657
- }
658
-
659
- /**
660
- * Return unserialized object or array
661
- *
662
- * @param string $serialized_string Serialized string.
663
- * @param string $method The name of the caller method.
664
- *
665
- * @return mixed, false on failure
666
- */
667
- private static function unserialize( $serialized_string ) {
668
- if( !is_serialized( $serialized_string ) ) {
669
- return false;
670
- }
671
-
672
- $serialized_string = trim( $serialized_string );
673
- $unserialized_string = @unserialize( $serialized_string );
674
-
675
- return $unserialized_string;
676
- }
677
-
678
- /**
679
- * Wrapper for str_replace
680
- *
681
- * @param string $from
682
- * @param string $to
683
- * @param string $data
684
- * @param string|bool $case_insensitive
685
- *
686
- * @return string
687
- */
688
- private function str_replace( $from, $to, $data, $case_insensitive = false ) {
689
-
690
- // Add filter
691
- $excludes = apply_filters( 'wpstg_clone_searchreplace_excl', array() );
692
-
693
- // Build pattern
694
- $regexExclude = '';
695
- foreach ( $excludes as $exclude ) {
696
- $regexExclude .= $exclude . '(*SKIP)(FAIL)|';
697
- }
698
-
699
- if( 'on' === $case_insensitive ) {
700
- //$data = str_ireplace( $from, $to, $data );
701
- $data = preg_replace( '#' . $regexExclude . preg_quote( $from ) . '#i', $to, $data );
702
- } else {
703
- //$data = str_replace( $from, $to, $data );
704
- $data = preg_replace( '#' . $regexExclude . preg_quote( $from ) . '#', $to, $data );
705
- }
706
-
707
- return $data;
708
- }
709
-
710
- /**
711
- * Set the job
712
- * @param string $table
713
- */
714
- private function setJob( $table ) {
715
- if( !empty( $this->options->job->current ) ) {
716
- return;
717
- }
718
-
719
- $this->options->job->current = $table;
720
- $this->options->job->start = 0;
721
- }
722
-
723
- /**
724
- * Start Job
725
- * @param string $new
726
- * @param string $old
727
- * @return bool
728
- */
729
- private function startJob( $new, $old ) {
730
-
731
- $this->options->job->total = 0;
732
-
733
- if( 0 != $this->options->job->start ) {
734
- return true;
735
- }
736
-
737
- // Table does not exists
738
- $result = $this->db->query( "SHOW TABLES LIKE '{$old}'" );
739
- if( !$result || 0 === $result ) {
740
- return false;
741
- }
742
-
743
- $this->options->job->total = ( int ) $this->db->get_var( "SELECT COUNT(1) FROM {$old}" );
744
-
745
- if( 0 == $this->options->job->total ) {
746
- $this->finishStep();
747
- return false;
748
- }
749
-
750
- return true;
751
- }
752
-
753
- /**
754
- * Finish the step
755
- */
756
- private function finishStep() {
757
- // This job is not finished yet
758
- if( $this->options->job->total > $this->options->job->start ) {
759
- return false;
760
- }
761
-
762
- // Add it to cloned tables listing
763
- $this->options->clonedTables[] = $this->options->tables[$this->options->currentStep];
764
-
765
- // Reset job
766
- $this->options->job = new \stdClass();
767
-
768
- return true;
769
- }
770
-
771
- /**
772
- * Drop table if necessary
773
- * @param string $new
774
- */
775
- private function dropTable( $new ) {
776
- $old = $this->db->get_var( $this->db->prepare( "SHOW TABLES LIKE %s", $new ) );
777
-
778
- if( !$this->shouldDropTable( $new, $old ) ) {
779
- return;
780
- }
781
-
782
- $this->log( "DB Processing: {$new} already exists, dropping it first" );
783
- $this->db->query( "DROP TABLE {$new}" );
784
- }
785
-
786
- /**
787
- * Check if table needs to be dropped
788
- * @param string $new
789
- * @param string $old
790
- * @return bool
791
- */
792
- private function shouldDropTable( $new, $old ) {
793
- return (
794
- $old == $new &&
795
- (
796
- !isset( $this->options->job->current ) ||
797
- !isset( $this->options->job->start ) ||
798
- 0 == $this->options->job->start
799
- )
800
- );
801
- }
802
-
803
- /**
804
- * Check if WP is installed in subdir
805
- * @return boolean
806
- */
807
- private function isSubDir() {
808
- // Compare names without scheme to bypass cases where siteurl and home have different schemes http / https
809
- // This is happening much more often than you would expect
810
- $siteurl = preg_replace( '#^https?://#', '', rtrim( get_option( 'siteurl' ), '/' ) );
811
- $home = preg_replace( '#^https?://#', '', rtrim( get_option( 'home' ), '/' ) );
812
-
813
- if( $home !== $siteurl ) {
814
- return true;
815
- }
816
- return false;
817
- }
818
-
819
- }
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
+ return trailingslashit( $this->strings->getUrlWithoutScheme($this->multisiteDomainWithoutScheme) ) . $this->options->cloneDirectoryName;
200
+ }
201
+
202
+ /**
203
+ * Get the install sub directory if WP is installed in sub directory
204
+ * @return string
205
+ */
206
+ private function getSubDir() {
207
+ $home = get_option( 'home' );
208
+ $siteurl = get_option( 'siteurl' );
209
+
210
+ if( empty( $home ) || empty( $siteurl ) ) {
211
+ return '';
212
+ }
213
+
214
+ $dir = str_replace( $home, '', $siteurl );
215
+ return str_replace( '/', '', $dir );
216
+ }
217
+
218
+ /**
219
+ * Start search replace job
220
+ * @param string $new
221
+ * @param string $old
222
+ */
223
+ private function startReplace( $table ) {
224
+ $rows = $this->options->job->start + $this->settings->querySRLimit;
225
+ $this->log(
226
+ "DB Processing: Table {$table} {$this->options->job->start} to {$rows} records"
227
+ );
228
+
229
+ // Search & Replace
230
+ $this->searchReplace( $table, $rows, array() );
231
+
232
+ // Set new offset
233
+ $this->options->job->start += $this->settings->querySRLimit;
234
+ }
235
+
236
+ /**
237
+ * Returns the number of pages in a table.
238
+ * @access public
239
+ * @return int
240
+ */
241
+ private function get_pages_in_table( $table ) {
242
+
243
+ // Table does not exists
244
+ $result = $this->db->query( "SHOW TABLES LIKE '{$table}'" );
245
+ if( !$result || 0 === $result ) {
246
+ return 0;
247
+ }
248
+
249
+ $table = esc_sql( $table );
250
+ $rows = $this->db->get_var( "SELECT COUNT(*) FROM $table" );
251
+ $pages = ceil( $rows / $this->settings->querySRLimit );
252
+ return absint( $pages );
253
+ }
254
+
255
+ /**
256
+ * Gets the columns in a table.
257
+ * @access public
258
+ * @param string $table The table to check.
259
+ * @return array
260
+ */
261
+ private function get_columns( $table ) {
262
+ $primary_key = null;
263
+ $columns = array();
264
+ $fields = $this->db->get_results( 'DESCRIBE ' . $table );
265
+ if( is_array( $fields ) ) {
266
+ foreach ( $fields as $column ) {
267
+ $columns[] = $column->Field;
268
+ if( $column->Key == 'PRI' ) {
269
+ $primary_key = $column->Field;
270
+ }
271
+ }
272
+ }
273
+ return array($primary_key, $columns);
274
+ }
275
+
276
+ /**
277
+ * Return absolute destination path
278
+ * @return string
279
+ */
280
+ private function getAbsDestination() {
281
+ if( empty( $this->options->cloneDir ) ) {
282
+ return \WPStaging\WPStaging::getWPpath();
283
+ }
284
+ return trailingslashit( $this->options->cloneDir );
285
+ }
286
+
287
+ /**
288
+ * Adapated from interconnect/it's search/replace script, adapted from Better Search Replace
289
+ *
290
+ * Modified to use WordPress wpdb functions instead of PHP's native mysql/pdo functions,
291
+ * and to be compatible with batch processing.
292
+ *
293
+ * @link https://interconnectit.com/products/search-and-replace-for-wordpress-databases/
294
+ *
295
+ * @access public
296
+ * @param string $table The table to run the replacement on.
297
+ * @param int $page The page/block to begin the query on.
298
+ * @param array $args An associative array containing arguments for this run.
299
+ * @return array
300
+ */
301
+ private function searchReplace( $table, $page, $args ) {
302
+
303
+ if( $this->thirdParty->isSearchReplaceExcluded( $table ) ) {
304
+ $this->log( "DB Processing: Skip {$table}", \WPStaging\Utils\Logger::TYPE_INFO );
305
+ return true;
306
+ }
307
+
308
+ // Load up the default settings for this chunk.
309
+ $table = esc_sql( $table );
310
+ $current_page = $this->options->job->start + $this->settings->querySRLimit;
311
+ $pages = $this->get_pages_in_table( $table );
312
+
313
+
314
+ // Search URL example.com/staging and root path to staging site /var/www/htdocs/staging
315
+ $args['search_for'] = array(
316
+ '//' . $this->getSourceHostname(),
317
+ ABSPATH,
318
+ '\/\/' . str_replace( '/', '\/', $this->getSourceHostname() ), // // Used by revslider and several visual editors
319
+ $this->getImagePathLive()
320
+ );
321
+
322
+
323
+ $args['replace_with'] = array(
324
+ '//' . $this->getDestinationHostname(),
325
+ $this->options->destinationDir,
326
+ '\/\/' . str_replace( '/', '\/', $this->getDestinationHostname() ), // Used by revslider and several visual editors
327
+ $this->getImagePathStaging()
328
+ );
329
+
330
+
331
+ ##########################
332
+ // if( $this->isSubDir() ) {
333
+ // // Search URL example.com/staging and root path to staging site /var/www/htdocs/staging
334
+ // $args['search_for'] = array(
335
+ // rtrim( $this->multisiteHomeUrlWithoutScheme, "/" ) . $this->getSubDir(),
336
+ // ABSPATH,
337
+ // str_replace( '/', '\/', rtrim( $this->multisiteHomeUrlWithoutScheme, '/' ) ) . str_replace( '/', '\/', $this->getSubDir() ), // // Used by revslider and several visual editors
338
+ // $this->getImagePathLive()
339
+ // );
340
+ //
341
+ //
342
+ // $args['replace_with'] = array(
343
+ // rtrim( $this->multisiteDomainWithoutScheme, "/" ) . $this->getSubDir() . '/' . $this->options->cloneDirectoryName,
344
+ // rtrim( $this->getAbsDestination(), '/' ) . '/' . $this->options->cloneDirectoryName,
345
+ // str_replace( '/', '\/', rtrim( $this->multisiteDomainWithoutScheme, "/" ) ) . str_replace( '/', '\/', $this->getSubDir() ) . '\/' . $this->options->cloneDirectoryName, // Used by revslider and several visual editors
346
+ // $this->getImagePathStaging()
347
+ // );
348
+ // } else {
349
+ // $args['search_for'] = array(
350
+ // rtrim( $this->multisiteHomeUrlWithoutScheme, '/' ),
351
+ // ABSPATH,
352
+ // str_replace( '/', '\/', rtrim( $this->multisiteHomeUrlWithoutScheme, '/' ) ),
353
+ // $this->getImagePathLive()
354
+ // );
355
+ // $args['replace_with'] = array(
356
+ // rtrim( $this->multisiteDomainWithoutScheme, '/' ) . '/' . $this->options->cloneDirectoryName,
357
+ // rtrim( $this->getAbsDestination(), '/' ) . '/' . $this->options->cloneDirectoryName,
358
+ // str_replace( '/', '\/', rtrim( $this->multisiteDomainWithoutScheme, '/' ) ) . '\/' . $this->options->cloneDirectoryName,
359
+ // $this->getImagePathStaging()
360
+ // );
361
+ // }
362
+
363
+
364
+
365
+
366
+ $args['replace_guids'] = 'off';
367
+ $args['dry_run'] = 'off';
368
+ $args['case_insensitive'] = false;
369
+ $args['replace_mails'] = 'off';
370
+ $args['skip_transients'] = 'on';
371
+
372
+
373
+ // Allow filtering of search & replace parameters
374
+ $args = apply_filters( 'wpstg_clone_searchreplace_params', $args );
375
+
376
+ // Get a list of columns in this table.
377
+ list( $primary_key, $columns ) = $this->get_columns( $table );
378
+
379
+ // Bail out early if there isn't a primary key.
380
+ // We commented this to search & replace through tables which have no primary keys like wp_revslider_slides
381
+ // @todo test this carefully. If it causes (performance) issues we need to activate it again!
382
+ // @since 2.4.4
383
+ // if( null === $primary_key ) {
384
+ // return false;
385
+ // }
386
+
387
+ $current_row = 0;
388
+ $start = $this->options->job->start;
389
+ $end = $this->settings->querySRLimit;
390
+
391
+ // Grab the content of the table.
392
+ $data = $this->db->get_results( "SELECT * FROM $table LIMIT $start, $end", ARRAY_A );
393
+
394
+ // Filter certain rows option_name in wpstg_options
395
+ $filter = array(
396
+ 'Admin_custome_login_Slidshow',
397
+ 'Admin_custome_login_Social',
398
+ 'Admin_custome_login_logo',
399
+ 'Admin_custome_login_text',
400
+ 'Admin_custome_login_login',
401
+ 'Admin_custome_login_top',
402
+ 'Admin_custome_login_dashboard',
403
+ 'Admin_custome_login_Version',
404
+ 'upload_path',
405
+ );
406
+
407
+ $filter = apply_filters( 'wpstg_clone_searchreplace_excl_rows', $filter );
408
+
409
+ // Loop through the data.
410
+ foreach ( $data as $row ) {
411
+ $current_row++;
412
+ $update_sql = array();
413
+ $where_sql = array();
414
+ $upd = false;
415
+
416
+ // Skip rows below
417
+ if( isset( $row['option_name'] ) && in_array( $row['option_name'], $filter ) ) {
418
+ continue;
419
+ }
420
+
421
+ // Skip rows with transients (They can store huge data and we need to save memory)
422
+ if( isset( $row['option_name'] ) && 'on' === $args['skip_transients'] && false !== strpos( $row['option_name'], '_transient' ) ) {
423
+ continue;
424
+ }
425
+
426
+ foreach ( $columns as $column ) {
427
+
428
+ $dataRow = $row[$column];
429
+
430
+ if( $column == $primary_key ) {
431
+ $where_sql[] = $column . ' = "' . $this->mysql_escape_mimic( $dataRow ) . '"';
432
+ continue;
433
+ }
434
+
435
+ // Skip GUIDs by default.
436
+ if( 'on' !== $args['replace_guids'] && 'guid' == $column ) {
437
+ continue;
438
+ }
439
+
440
+ // Skip mail addresses
441
+ if( 'off' === $args['replace_mails'] && false !== strpos( $dataRow, '@' . $this->multisiteDomainWithoutScheme ) ) {
442
+ continue;
443
+ }
444
+
445
+ // Check options table
446
+ if( $this->options->prefix . 'options' === $table ) {
447
+
448
+ // Skip certain options
449
+ if( isset( $should_skip ) && true === $should_skip ) {
450
+ $should_skip = false;
451
+ continue;
452
+ }
453
+
454
+ // Skip this row
455
+ if( 'wpstg_existing_clones_beta' === $dataRow ||
456
+ 'wpstg_existing_clones' === $dataRow ||
457
+ 'wpstg_settings' === $dataRow ||
458
+ 'wpstg_license_status' === $dataRow ||
459
+ 'siteurl' === $dataRow ||
460
+ 'home' === $dataRow
461
+ ) {
462
+ $should_skip = true;
463
+ }
464
+ }
465
+
466
+ // Check the path delimiter for / or \/ and remove one of those which prevents from resulting in wrong syntax like domain.com/staging\/.
467
+ // 1. local.wordpress.test -> local.wordpress.test/staging
468
+ // 2. local.wordpress.test\/ -> local.wordpress.test\/staging\/
469
+ $tmp = $args;
470
+ if( false === strpos( $dataRow, $tmp['search_for'][0] ) ) {
471
+ array_shift( $tmp['search_for'] ); // rtrim( $this->homeUrl, '/' ),
472
+ array_shift( $tmp['replace_with'] ); // rtrim( $this->homeUrl, '/' ) . '/' . $this->options->cloneDirectoryName,
473
+ } else {
474
+ unset( $tmp['search_for'][1] );
475
+ unset( $tmp['replace_with'][1] );
476
+ // recount array
477
+ $tmp['search_for'] = array_values( $tmp['search_for'] );
478
+ $tmp['replace_with'] = array_values( $tmp['replace_with'] );
479
+ }
480
+
481
+ // Run a search replace on the data row and respect the serialisation.
482
+ $i = 0;
483
+ foreach ( $tmp['search_for'] as $replace ) {
484
+ $dataRow = $this->recursive_unserialize_replace( $tmp['search_for'][$i], $tmp['replace_with'][$i], $dataRow, false, $args['case_insensitive'] );
485
+ $i++;
486
+ }
487
+ unset( $replace );
488
+ unset( $i );
489
+ unset( $tmp );
490
+
491
+ // Something was changed
492
+ if( $row[$column] != $dataRow ) {
493
+ $update_sql[] = $column . ' = "' . $this->mysql_escape_mimic( $dataRow ) . '"';
494
+ $upd = true;
495
+ }
496
+ }
497
+
498
+ // Determine what to do with updates.
499
+ if( $args['dry_run'] === 'on' ) {
500
+ // Don't do anything if a dry run
501
+ } elseif( $upd && !empty( $where_sql ) ) {
502
+ // If there are changes to make, run the query.
503
+ $sql = 'UPDATE ' . $table . ' SET ' . implode( ', ', $update_sql ) . ' WHERE ' . implode( ' AND ', array_filter( $where_sql ) );
504
+ $result = $this->db->query( $sql );
505
+
506
+ if( !$result ) {
507
+ $this->log( "Error updating row {$current_row} SQL: {$sql}", \WPStaging\Utils\Logger::TYPE_ERROR );
508
+ }
509
+ }
510
+ } // end row loop
511
+ unset( $row );
512
+ unset( $update_sql );
513
+ unset( $where_sql );
514
+ unset( $sql );
515
+
516
+
517
+ // DB Flush
518
+ $this->db->flush();
519
+ return true;
520
+ }
521
+
522
+ /**
523
+ * Get path to multisite image folder e.g. wp-content/blogs.dir/ID/files or wp-content/uploads/sites/ID
524
+ * @return string
525
+ */
526
+ private function getImagePathLive() {
527
+ // Check first which structure is used
528
+ $uploads = wp_upload_dir();
529
+ $basedir = $uploads['basedir'];
530
+ $blogId = get_current_blog_id();
531
+
532
+ if( false === strpos( $basedir, 'blogs.dir' ) ) {
533
+ // Since WP 3.5
534
+ $path = $blogId > 1 ?
535
+ 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'sites' . DIRECTORY_SEPARATOR . get_current_blog_id() . DIRECTORY_SEPARATOR :
536
+ 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR;
537
+ } else {
538
+ // old blog structure
539
+ $path = $blogId > 1 ?
540
+ 'wp-content' . DIRECTORY_SEPARATOR . 'blogs.dir' . DIRECTORY_SEPARATOR . get_current_blog_id() . DIRECTORY_SEPARATOR . 'files' . DIRECTORY_SEPARATOR :
541
+ 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR;
542
+ }
543
+ return $path;
544
+ }
545
+
546
+ /**
547
+ * Get path to staging site image path wp-content/uploads
548
+ * @return string
549
+ */
550
+ private function getImagePathStaging() {
551
+ return 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR;
552
+ }
553
+
554
+ /**
555
+ * Adapted from interconnect/it's search/replace script.
556
+ *
557
+ * @link https://interconnectit.com/products/search-and-replace-for-wordpress-databases/
558
+ *
559
+ * Take a serialised array and unserialise it replacing elements as needed and
560
+ * unserialising any subordinate arrays and performing the replace on those too.
561
+ *
562
+ * @access private
563
+ * @param string $from String we're looking to replace.
564
+ * @param string $to What we want it to be replaced with
565
+ * @param array $data Used to pass any subordinate arrays back to in.
566
+ * @param boolean $serialized Does the array passed via $data need serialising.
567
+ * @param sting|boolean $case_insensitive Set to 'on' if we should ignore case, false otherwise.
568
+ *
569
+ * @return string|array The original array with all elements replaced as needed.
570
+ */
571
+ private function recursive_unserialize_replace( $from = '', $to = '', $data = '', $serialized = false, $case_insensitive = false ) {
572
+ try {
573
+ // Some unserialized data cannot be re-serialized eg. SimpleXMLElements
574
+ if( is_serialized( $data ) && ( $unserialized = @unserialize( $data ) ) !== false ) {
575
+ $data = $this->recursive_unserialize_replace( $from, $to, $unserialized, true, $case_insensitive );
576
+ } elseif( is_array( $data ) ) {
577
+ $tmp = array();
578
+ foreach ( $data as $key => $value ) {
579
+ $tmp[$key] = $this->recursive_unserialize_replace( $from, $to, $value, false, $case_insensitive );
580
+ }
581
+
582
+ $data = $tmp;
583
+ unset( $tmp );
584
+ } elseif( is_object( $data ) ) {
585
+ $tmp = $data;
586
+ $props = get_object_vars( $data );
587
+ foreach ( $props as $key => $value ) {
588
+ if( $key === '' || ord( $key[0] ) === 0 ) {
589
+ continue;
590
+ }
591
+ $tmp->$key = $this->recursive_unserialize_replace( $from, $to, $value, false, $case_insensitive );
592
+ }
593
+
594
+ $data = $tmp;
595
+ unset( $tmp );
596
+ } else {
597
+ if( is_string( $data ) ) {
598
+ if( !empty( $from ) && !empty( $to ) ) {
599
+ $data = $this->str_replace( $from, $to, $data, $case_insensitive );
600
+ }
601
+ }
602
+ }
603
+
604
+ if( $serialized ) {
605
+ return serialize( $data );
606
+ }
607
+ } catch ( Exception $error ) {
608
+
609
+ }
610
+
611
+ return $data;
612
+ }
613
+
614
+ /**
615
+ * Check if the object is a valid one and not __PHP_Incomplete_Class_Name
616
+ * Can not use is_object alone because in php 7.2 it's returning true even though object is __PHP_Incomplete_Class_Name
617
+ * @return boolean
618
+ */
619
+ // private function isValidObject( $data ) {
620
+ // if( !is_object( $data ) || gettype( $data ) != 'object' ) {
621
+ // return false;
622
+ // }
623
+ //
624
+ // $invalid_class_props = get_object_vars( $data );
625
+ //
626
+ // if (!isset($invalid_class_props['__PHP_Incomplete_Class_Name'])){
627
+ // // Assume it must be an valid object
628
+ // return true;
629
+ // }
630
+ //
631
+ // $invalid_object_class = $invalid_class_props['__PHP_Incomplete_Class_Name'];
632
+ //
633
+ // if( !empty( $invalid_object_class ) ) {
634
+ // return false;
635
+ // }
636
+ //
637
+ // // Assume it must be an valid object
638
+ // return true;
639
+ // }
640
+
641
+ /**
642
+ * Mimics the mysql_real_escape_string function. Adapted from a post by 'feedr' on php.net.
643
+ * @link http://php.net/manual/en/function.mysql-real-escape-string.php#101248
644
+ * @access public
645
+ * @param string $input The string to escape.
646
+ * @return string
647
+ */
648
+ private function mysql_escape_mimic( $input ) {
649
+ if( is_array( $input ) ) {
650
+ return array_map( __METHOD__, $input );
651
+ }
652
+ if( !empty( $input ) && is_string( $input ) ) {
653
+ return str_replace( array('\\', "\0", "\n", "\r", "'", '"', "\x1a"), array('\\\\', '\\0', '\\n', '\\r', "\\'", '\\"', '\\Z'), $input );
654
+ }
655
+
656
+ return $input;
657
+ }
658
+
659
+ /**
660
+ * Return unserialized object or array
661
+ *
662
+ * @param string $serialized_string Serialized string.
663
+ * @param string $method The name of the caller method.
664
+ *
665
+ * @return mixed, false on failure
666
+ */
667
+ private static function unserialize( $serialized_string ) {
668
+ if( !is_serialized( $serialized_string ) ) {
669
+ return false;
670
+ }
671
+
672
+ $serialized_string = trim( $serialized_string );
673
+ $unserialized_string = @unserialize( $serialized_string );
674
+
675
+ return $unserialized_string;
676
+ }
677
+
678
+ /**
679
+ * Wrapper for str_replace
680
+ *
681
+ * @param string $from
682
+ * @param string $to
683
+ * @param string $data
684
+ * @param string|bool $case_insensitive
685
+ *
686
+ * @return string
687
+ */
688
+ private function str_replace( $from, $to, $data, $case_insensitive = false ) {
689
+
690
+ // Add filter
691
+ $excludes = apply_filters( 'wpstg_clone_searchreplace_excl', array() );
692
+
693
+ // Build pattern
694
+ $regexExclude = '';
695
+ foreach ( $excludes as $exclude ) {
696
+ $regexExclude .= $exclude . '(*SKIP)(FAIL)|';
697
+ }
698
+
699
+ if( 'on' === $case_insensitive ) {
700
+ //$data = str_ireplace( $from, $to, $data );
701
+ $data = preg_replace( '#' . $regexExclude . preg_quote( $from ) . '#i', $to, $data );
702
+ } else {
703
+ //$data = str_replace( $from, $to, $data );
704
+ $data = preg_replace( '#' . $regexExclude . preg_quote( $from ) . '#', $to, $data );
705
+ }
706
+
707
+ return $data;
708
+ }
709
+
710
+ /**
711
+ * Set the job
712
+ * @param string $table
713
+ */
714
+ private function setJob( $table ) {
715
+ if( !empty( $this->options->job->current ) ) {
716
+ return;
717
+ }
718
+
719
+ $this->options->job->current = $table;
720
+ $this->options->job->start = 0;
721
+ }
722
+
723
+ /**
724
+ * Start Job
725
+ * @param string $new
726
+ * @param string $old
727
+ * @return bool
728
+ */
729
+ private function startJob( $new, $old ) {
730
+
731
+ $this->options->job->total = 0;
732
+
733
+ if( 0 != $this->options->job->start ) {
734
+ return true;
735
+ }
736
+
737
+ // Table does not exists
738
+ $result = $this->db->query( "SHOW TABLES LIKE '{$old}'" );
739
+ if( !$result || 0 === $result ) {
740
+ return false;
741
+ }
742
+
743
+ $this->options->job->total = ( int ) $this->db->get_var( "SELECT COUNT(1) FROM {$old}" );
744
+
745
+ if( 0 == $this->options->job->total ) {
746
+ $this->finishStep();
747
+ return false;
748
+ }
749
+
750
+ return true;
751
+ }
752
+
753
+ /**
754
+ * Finish the step
755
+ */
756
+ private function finishStep() {
757
+ // This job is not finished yet
758
+ if( $this->options->job->total > $this->options->job->start ) {
759
+ return false;
760
+ }
761
+
762
+ // Add it to cloned tables listing
763
+ $this->options->clonedTables[] = $this->options->tables[$this->options->currentStep];
764
+
765
+ // Reset job
766
+ $this->options->job = new \stdClass();
767
+
768
+ return true;
769
+ }
770
+
771
+ /**
772
+ * Drop table if necessary
773
+ * @param string $new
774
+ */
775
+ private function dropTable( $new ) {
776
+ $old = $this->db->get_var( $this->db->prepare( "SHOW TABLES LIKE %s", $new ) );
777
+
778
+ if( !$this->shouldDropTable( $new, $old ) ) {
779
+ return;
780
+ }
781
+
782
+ $this->log( "DB Processing: {$new} already exists, dropping it first" );
783
+ $this->db->query( "DROP TABLE {$new}" );
784
+ }
785
+
786
+ /**
787
+ * Check if table needs to be dropped
788
+ * @param string $new
789
+ * @param string $old
790
+ * @return bool
791
+ */
792
+ private function shouldDropTable( $new, $old ) {
793
+ return (
794
+ $old == $new &&
795
+ (
796
+ !isset( $this->options->job->current ) ||
797
+ !isset( $this->options->job->start ) ||
798
+ 0 == $this->options->job->start
799
+ )
800
+ );
801
+ }
802
+
803
+ /**
804
+ * Check if WP is installed in subdir
805
+ * @return boolean
806
+ */
807
+ private function isSubDir() {
808
+ // Compare names without scheme to bypass cases where siteurl and home have different schemes http / https
809
+ // This is happening much more often than you would expect
810
+ $siteurl = preg_replace( '#^https?://#', '', rtrim( get_option( 'siteurl' ), '/' ) );
811
+ $home = preg_replace( '#^https?://#', '', rtrim( get_option( 'home' ), '/' ) );
812
+
813
+ if( $home !== $siteurl ) {
814
+ return true;
815
+ }
816
+ return false;
817
+ }
818
+
819
+ }
apps/Backend/Modules/Jobs/Multisite/SearchReplaceExternal.php CHANGED
@@ -1,848 +1,848 @@
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 Processing: 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 exists
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
- * Return absolute destination path
287
- * @return string
288
- */
289
- // private function getAbsDestination() {
290
- // if( empty( $this->options->cloneDir ) ) {
291
- // return \WPStaging\WPStaging::getWPpath();
292
- // }
293
- // return trailingslashit( $this->options->cloneDir );
294
- // }
295
-
296
- /**
297
- * Adapated from interconnect/it's search/replace script, adapted from Better Search Replace
298
- *
299
- * Modified to use WordPress wpdb functions instead of PHP's native mysql/pdo functions,
300
- * and to be compatible with batch processing.
301
- *
302
- * @link https://interconnectit.com/products/search-and-replace-for-wordpress-databases/
303
- *
304
- * @access public
305
- * @param string $table The table to run the replacement on.
306
- * @param int $page The page/block to begin the query on.
307
- * @param array $args An associative array containing arguments for this run.
308
- * @return array
309
- */
310
- private function searchReplace( $table, $page, $args ) {
311
-
312
- if( $this->thirdParty->isSearchReplaceExcluded( $table ) ) {
313
- $this->log( "DB Processing: Skip {$table}", \WPStaging\Utils\Logger::TYPE_INFO );
314
- return true;
315
- }
316
-
317
- // Load up the default settings for this chunk.
318
- $table = esc_sql( $table );
319
- $current_page = $this->options->job->start + $this->settings->querySRLimit;
320
- $pages = $this->get_pages_in_table( $table );
321
-
322
-
323
- // Search URL example.com/staging and root path to staging site /var/www/htdocs/staging
324
- $args['search_for'] = array(
325
- '//' . $this->sourceHostname,
326
- ABSPATH,
327
- '\/\/' . str_replace( '/', '\/', $this->sourceHostname ), // // Used by revslider and several visual editors
328
- $this->getImagePathLive()
329
- );
330
-
331
-
332
- $args['replace_with'] = array(
333
- '//' . $this->destinationHostname,
334
- $this->options->destinationDir,
335
- '\/\/' . str_replace( '/', '\/', $this->destinationHostname ), // Used by revslider and several visual editors
336
- $this->getImagePathStaging()
337
- );
338
-
339
- // if( $this->isSubDir() ) {
340
- // // Search URL example.com/staging and root path to staging site /var/www/htdocs/staging
341
- // $args['search_for'] = array(
342
- // '//' . rtrim( $this->multisiteHomeUrlWithoutScheme, "/" ) . $this->getSubDir(),
343
- // ABSPATH,
344
- // str_replace( '/', '\/', rtrim( $this->multisiteHomeUrlWithoutScheme, '/' ) ) . str_replace( '/', '\/', $this->getSubDir() ), // // Used by revslider and several visual editors
345
- // $this->getImagePathLive()
346
- // );
347
- //
348
- //
349
- // $args['replace_with'] = array(
350
- // '//' . rtrim( $this->multisiteDomainWithoutScheme, "/" ) . $this->getSubDir() . '/' . $this->options->cloneDirectoryName,
351
- // rtrim( $this->getAbsDestination(), '/' ) . '/' . $this->options->cloneDirectoryName,
352
- // str_replace( '/', '\/', rtrim( $this->multisiteDomainWithoutScheme, "/" ) ) . str_replace( '/', '\/', $this->getSubDir() ) . '\/' . $this->options->cloneDirectoryName, // Used by revslider and several visual editors
353
- // $this->getImagePathStaging()
354
- // );
355
- // } else {
356
- // $args['search_for'] = array(
357
- // '//' . rtrim( $this->multisiteHomeUrlWithoutScheme, '/' ),
358
- // ABSPATH,
359
- // str_replace( '/', '\/', rtrim( $this->multisiteHomeUrlWithoutScheme, '/' ) ),
360
- // $this->getImagePathLive()
361
- // );
362
- // $args['replace_with'] = array(
363
- // '//' . rtrim( $this->multisiteDomainWithoutScheme, '/' ) . '/' . $this->options->cloneDirectoryName,
364
- // rtrim( $this->getAbsDestination(), '/' ) . '/' . $this->options->cloneDirectoryName,
365
- // str_replace( '/', '\/', rtrim( $this->multisiteDomainWithoutScheme, '/' ) ) . '\/' . $this->options->cloneDirectoryName,
366
- // $this->getImagePathStaging()
367
- // );
368
- // }
369
-
370
- $args['replace_guids'] = 'off';
371
- $args['dry_run'] = 'off';
372
- $args['case_insensitive'] = false;
373
- $args['replace_mails'] = 'off';
374
- $args['skip_transients'] = 'on';
375
-
376
-
377
- // Allow filtering of search & replace parameters
378
- $args = apply_filters( 'wpstg_clone_searchreplace_params', $args );
379
-
380
- // Get a list of columns in this table.
381
- list( $primary_key, $columns ) = $this->get_columns( $table );
382
-
383
- // Bail out early if there isn't a primary key.
384
- // We commented this to search & replace through tables which have no primary keys like wp_revslider_slides
385
- // @todo test this carefully. If it causes (performance) issues we need to activate it again!
386
- // @since 2.4.4
387
- // if( null === $primary_key ) {
388
- // return false;
389
- // }
390
-
391
- $current_row = 0;
392
- $start = $this->options->job->start;
393
- $end = $this->settings->querySRLimit;
394
-
395
- // Grab the content of the table.
396
- $data = $this->stagingDb->get_results( "SELECT * FROM $table LIMIT $start, $end", ARRAY_A );
397
-
398
- // Filter certain rows (of other plugins)
399
- $filter = array(
400
- 'Admin_custome_login_Slidshow',
401
- 'Admin_custome_login_Social',
402
- 'Admin_custome_login_logo',
403
- 'Admin_custome_login_text',
404
- 'Admin_custome_login_login',
405
- 'Admin_custome_login_top',
406
- 'Admin_custome_login_dashboard',
407
- 'Admin_custome_login_Version',
408
- 'upload_path',
409
- );
410
-
411
- $filter = apply_filters( 'wpstg_clone_searchreplace_excl_rows', $filter );
412
-
413
- // Loop through the data.
414
- foreach ( $data as $row ) {
415
- $current_row++;
416
- $update_sql = array();
417
- $where_sql = array();
418
- $upd = false;
419
-
420
- // Skip rows below
421
- if( isset( $row['option_name'] ) && in_array( $row['option_name'], $filter ) ) {
422
- continue;
423
- }
424
-
425
- // Skip rows with transients (They can store huge data and we need to save memory)
426
- if( isset( $row['option_name'] ) && 'on' === $args['skip_transients'] && false !== strpos( $row['option_name'], '_transient' ) ) {
427
- continue;
428
- }
429
-
430
- foreach ( $columns as $column ) {
431
-
432
- $dataRow = $row[$column];
433
-
434
- if( $column == $primary_key ) {
435
- $where_sql[] = $column . ' = "' . $this->mysql_escape_mimic( $dataRow ) . '"';
436
- continue;
437
- }
438
-
439
- // Skip GUIDs by default.
440
- if( 'on' !== $args['replace_guids'] && 'guid' == $column ) {
441
- continue;
442
- }
443
-
444
- // Skip mail addresses
445
- if( 'off' === $args['replace_mails'] && false !== strpos( $dataRow, '@' . $this->multisiteDomainWithoutScheme ) ) {
446
- continue;
447
- }
448
-
449
- // Check options table
450
- if( $this->options->prefix . 'options' === $table ) {
451
-
452
- // Skip certain options
453
- if( isset( $should_skip ) && true === $should_skip ) {
454
- $should_skip = false;
455
- continue;
456
- }
457
-
458
- // Skip this row
459
- if( 'wpstg_existing_clones_beta' === $dataRow ||
460
- 'wpstg_existing_clones' === $dataRow ||
461
- 'wpstg_settings' === $dataRow ||
462
- 'wpstg_license_status' === $dataRow ||
463
- 'siteurl' === $dataRow ||
464
- 'home' === $dataRow
465
- ) {
466
- $should_skip = true;
467
- }
468
- }
469
-
470
- // Check the path delimiter for / or \/ and remove one of those which prevents from resulting in wrong syntax like domain.com/staging\/.
471
- // 1. local.wordpress.test -> local.wordpress.test/staging
472
- // 2. local.wordpress.test\/ -> local.wordpress.test\/staging\/
473
- $tmp = $args;
474
- if( false === strpos( $dataRow, $tmp['search_for'][0] ) ) {
475
- array_shift( $tmp['search_for'] ); // rtrim( $this->homeUrl, '/' ),
476
- array_shift( $tmp['replace_with'] ); // rtrim( $this->homeUrl, '/' ) . '/' . $this->options->cloneDirectoryName,
477
- } else {
478
- unset( $tmp['search_for'][1] );
479
- unset( $tmp['replace_with'][1] );
480
- // recount array
481
- $tmp['search_for'] = array_values( $tmp['search_for'] );
482
- $tmp['replace_with'] = array_values( $tmp['replace_with'] );
483
- }
484
-
485
- // Run a search replace on the data row and respect the serialisation.
486
- $i = 0;
487
- foreach ( $tmp['search_for'] as $replace ) {
488
- $dataRow = $this->recursive_unserialize_replace( $tmp['search_for'][$i], $tmp['replace_with'][$i], $dataRow, false, $args['case_insensitive'] );
489
- $i++;
490
- }
491
- unset( $replace );
492
- unset( $i );
493
- unset( $tmp );
494
-
495
- // Something was changed
496
- if( $row[$column] != $dataRow ) {
497
- $update_sql[] = $column . ' = "' . $this->mysql_escape_mimic( $dataRow ) . '"';
498
- $upd = true;
499
- }
500
- }
501
-
502
- // Determine what to do with updates.
503
- if( $args['dry_run'] === 'on' ) {
504
- // Don't do anything if a dry run
505
- } elseif( $upd && !empty( $where_sql ) ) {
506
- // If there are changes to make, run the query.
507
- $sql = 'UPDATE ' . $table . ' SET ' . implode( ', ', $update_sql ) . ' WHERE ' . implode( ' AND ', array_filter( $where_sql ) );
508
- $result = $this->stagingDb->query( $sql );
509
-
510
- if( !$result ) {
511
- $this->log( "Error updating row {$current_row} SQL: {$sql}", \WPStaging\Utils\Logger::TYPE_ERROR );
512
- }
513
- }
514
- } // end row loop
515
- unset( $row );
516
- unset( $update_sql );
517
- unset( $where_sql );
518
- unset( $sql );
519
-
520
-
521
- // DB Flush
522
- $this->stagingDb->flush();
523
- return true;
524
- }
525
-
526
- /**
527
- * Get path to multisite image folder e.g. wp-content/blogs.dir/ID/files or wp-content/uploads/sites/ID
528
- * @return string
529
- */
530
- private function getImagePathLive() {
531
- // Check first which structure is used
532
- $uploads = wp_upload_dir();
533
- $basedir = $uploads['basedir'];
534
- $blogId = get_current_blog_id();
535
-
536
- if( false === strpos( $basedir, 'blogs.dir' ) ) {
537
- // Since WP 3.5
538
- $path = $blogId > 1 ?
539
- 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'sites' . DIRECTORY_SEPARATOR . get_current_blog_id() . DIRECTORY_SEPARATOR :
540
- 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR;
541
- } else {
542
- // old blog structure
543
- $path = $blogId > 1 ?
544
- 'wp-content' . DIRECTORY_SEPARATOR . 'blogs.dir' . DIRECTORY_SEPARATOR . get_current_blog_id() . DIRECTORY_SEPARATOR . 'files' . DIRECTORY_SEPARATOR :
545
- 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR;
546
- }
547
- return $path;
548
- }
549
-
550
- /**
551
- * Get path to staging site image path wp-content/uploads
552
- * @return string
553
- */
554
- private function getImagePathStaging() {
555
- return 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR;
556
- }
557
-
558
- /**
559
- * Adapted from interconnect/it's search/replace script.
560
- *
561
- * @link https://interconnectit.com/products/search-and-replace-for-wordpress-databases/
562
- *
563
- * Take a serialised array and unserialise it replacing elements as needed and
564
- * unserialising any subordinate arrays and performing the replace on those too.
565
- *
566
- * @access private
567
- * @param string $from String we're looking to replace.
568
- * @param string $to What we want it to be replaced with
569
- * @param array $data Used to pass any subordinate arrays back to in.
570
- * @param boolean $serialized Does the array passed via $data need serialising.
571
- * @param sting|boolean $case_insensitive Set to 'on' if we should ignore case, false otherwise.
572
- *
573
- * @return string|array The original array with all elements replaced as needed.
574
- */
575
- private function recursive_unserialize_replace( $from = '', $to = '', $data = '', $serialized = false, $case_insensitive = false ) {
576
- try {
577
- // Some unserialized data cannot be re-serialized eg. SimpleXMLElements
578
- if( is_serialized( $data ) && ( $unserialized = @unserialize( $data ) ) !== false ) {
579
- $data = $this->recursive_unserialize_replace( $from, $to, $unserialized, true, $case_insensitive );
580
- } elseif( is_array( $data ) ) {
581
- $tmp = array();
582
- foreach ( $data as $key => $value ) {
583
- $tmp[$key] = $this->recursive_unserialize_replace( $from, $to, $value, false, $case_insensitive );
584
- }
585
-
586
- $data = $tmp;
587
- unset( $tmp );
588
- } elseif( is_object( $data ) ) {
589
- $tmp = $data;
590
- $props = get_object_vars( $data );
591
- foreach ( $props as $key => $value ) {
592
- if( $key === '' || ord( $key[0] ) === 0 ) {
593
- continue;
594
- }
595
- $tmp->$key = $this->recursive_unserialize_replace( $from, $to, $value, false, $case_insensitive );
596
- }
597
-
598
- $data = $tmp;
599
- unset( $tmp );
600
- } else {
601
- if( is_string( $data ) ) {
602
- if( !empty( $from ) && !empty( $to ) ) {
603
- $data = $this->str_replace( $from, $to, $data, $case_insensitive );
604
- }
605
- }
606
- }
607
-
608
- if( $serialized ) {
609
- return serialize( $data );
610
- }
611
- } catch ( Exception $error ) {
612
-
613
- }
614
-
615
- return $data;
616
- }
617
-
618
- /**
619
- * Check if the object is a valid one and not __PHP_Incomplete_Class_Name
620
- * Can not use is_object alone because in php 7.2 it's returning true even though object is __PHP_Incomplete_Class_Name
621
- * @return boolean
622
- */
623
- // private function isValidObject( $data ) {
624
- // if( !is_object( $data ) || gettype( $data ) != 'object' ) {
625
- // return false;
626
- // }
627
- //
628
- // $invalid_class_props = get_object_vars( $data );
629
- //
630
- // if (!isset($invalid_class_props['__PHP_Incomplete_Class_Name'])){
631
- // // Assume it must be an valid object
632
- // return true;
633
- // }
634
- //
635
- // $invalid_object_class = $invalid_class_props['__PHP_Incomplete_Class_Name'];
636
- //
637
- // if( !empty( $invalid_object_class ) ) {
638
- // return false;
639
- // }
640
- //
641
- // // Assume it must be an valid object
642
- // return true;
643
- // }
644
-
645
- /**
646
- * Mimics the mysql_real_escape_string function. Adapted from a post by 'feedr' on php.net.
647
- * @link http://php.net/manual/en/function.mysql-real-escape-string.php#101248
648
- * @access public
649
- * @param string $input The string to escape.
650
- * @return string
651
- */
652
- private function mysql_escape_mimic( $input ) {
653
- if( is_array( $input ) ) {
654
- return array_map( __METHOD__, $input );
655
- }
656
- if( !empty( $input ) && is_string( $input ) ) {
657
- return str_replace( array('\\', "\0", "\n", "\r", "'", '"', "\x1a"), array('\\\\', '\\0', '\\n', '\\r', "\\'", '\\"', '\\Z'), $input );
658
- }
659
-
660
- return $input;
661
- }
662
-
663
- /**
664
- * Return unserialized object or array
665
- *
666
- * @param string $serialized_string Serialized string.
667
- * @param string $method The name of the caller method.
668
- *
669
- * @return mixed, false on failure
670
- */
671
- private static function unserialize( $serialized_string ) {
672
- if( !is_serialized( $serialized_string ) ) {
673
- return false;
674
- }
675
-
676
- $serialized_string = trim( $serialized_string );
677
- $unserialized_string = @unserialize( $serialized_string );
678
-
679
- return $unserialized_string;
680
- }
681
-
682
- /**
683
- * Wrapper for str_replace
684
- *
685
- * @param string $from
686
- * @param string $to
687
- * @param string $data
688
- * @param string|bool $case_insensitive
689
- *
690
- * @return string
691
- */
692
- private function str_replace( $from, $to, $data, $case_insensitive = false ) {
693
-
694
- // Add filter
695
- $excludes = apply_filters( 'wpstg_clone_searchreplace_excl', array() );
696
-
697
- // Build pattern
698
- $regexExclude = '';
699
- foreach ( $excludes as $exclude ) {
700
- $regexExclude .= $exclude . '(*SKIP)(FAIL)|';
701
- }
702
-
703
- if( 'on' === $case_insensitive ) {
704
- //$data = str_ireplace( $from, $to, $data );
705
- $data = preg_replace( '#' . $regexExclude . preg_quote( $from ) . '#i', $to, $data );
706
- } else {
707
- //$data = str_replace( $from, $to, $data );
708
- $data = preg_replace( '#' . $regexExclude . preg_quote( $from ) . '#', $to, $data );
709
- }
710
-
711
- return $data;
712
- }
713
-
714
- /**
715
- * Set the job
716
- * @param string $table
717
- */
718
- private function setJob( $table ) {
719
- if( !empty( $this->options->job->current ) ) {
720
- return;
721
- }
722
-
723
- $this->options->job->current = $table;
724
- $this->options->job->start = 0;
725
- }
726
-
727
- /**
728
- * Start Job
729
- * @param string $new
730
- * @param string $old
731
- * @return bool
732
- */
733
- private function startJob( $new, $old ) {
734
-
735
- if( $this->isExcludedTable( $new ) ) {
736
- return false;
737
- }
738
-
739
- // Table does not exists
740
- $result = $this->productionDb->query( "SHOW TABLES LIKE '{$old}'" );
741
- if( !$result || 0 === $result ) {
742
- return false;
743
- }
744
-
745
- if( 0 != $this->options->job->start ) {
746
- return true;
747
- }
748
-
749
- $this->options->job->total = ( int ) $this->productionDb->get_var( "SELECT COUNT(1) FROM {$old}" );
750
-
751
- if( 0 == $this->options->job->total ) {
752
- $this->finishStep();
753
- return false;
754
- }
755
-
756
- return true;
757
- }
758
-
759
- /**
760
- * Is table excluded from search replace processing?
761
- * @param string $table
762
- * @return boolean
763
- */
764
- private function isExcludedTable( $table ) {
765
-
766
- $customTables = apply_filters( 'wpstg_clone_searchreplace_tables_exclude', array() );
767
- $defaultTables = array('blogs');
768
-
769
- $tables = array_merge( $customTables, $defaultTables );
770
-
771
- $excludedTables = array();
772
- foreach ( $tables as $key => $value ) {
773
- $excludedTables[] = $this->options->prefix . $value;
774
- }
775
-
776
- if( in_array( $table, $excludedTables ) ) {
777
- return true;
778
- }
779
- return false;
780
- }
781
-
782
- /**
783
- * Finish the step
784
- */
785
- private function finishStep() {
786
- // This job is not finished yet
787
- if( $this->options->job->total > $this->options->job->start ) {
788
- return false;
789
- }
790
-
791
- // Add it to cloned tables listing
792
- $this->options->clonedTables[] = $this->options->tables[$this->options->currentStep];
793
-
794
- // Reset job
795
- $this->options->job = new \stdClass();
796
-
797
- return true;
798
- }
799
-
800
- /**
801
- * Drop table if necessary
802
- * @param string $new
803
- */
804
- private function dropTable( $new ) {
805
- $old = $this->stagingDb->get_var( $this->stagingDb->prepare( "SHOW TABLES LIKE %s", $new ) );
806
-
807
- if( !$this->shouldDropTable( $new, $old ) ) {
808
- return;
809
- }
810
-
811
- $this->log( "DB Processing: {$new} already exists, dropping it first" );
812
- $this->stagingDb->query( "DROP TABLE {$new}" );
813
- }
814
-
815
- /**
816
- * Check if table needs to be dropped
817
- * @param string $new
818
- * @param string $old
819
- * @return bool
820
- */
821
- private function shouldDropTable( $new, $old ) {
822
- return (
823
- $old == $new &&
824
- (
825
- !isset( $this->options->job->current ) ||
826
- !isset( $this->options->job->start ) ||
827
- 0 == $this->options->job->start
828
- )
829
- );
830
- }
831
-
832
- /**
833
- * Check if WP is installed in subdir
834
- * @return boolean
835
- */
836
- private function isSubDir() {
837
- // Compare names without scheme to bypass cases where siteurl and home have different schemes http / https
838
- // This is happening much more often than you would expect
839
- $siteurl = preg_replace( '#^https?://#', '', rtrim( get_option( 'siteurl' ), '/' ) );
840
- $home = preg_replace( '#^https?://#', '', rtrim( get_option( 'home' ), '/' ) );
841
-
842
- if( $home !== $siteurl ) {
843
- return true;
844
- }
845
- return false;
846
- }
847
-
848
- }
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 Processing: 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 exists
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
+ * Return absolute destination path
287
+ * @return string
288
+ */
289
+ // private function getAbsDestination() {
290
+ // if( empty( $this->options->cloneDir ) ) {
291
+ // return \WPStaging\WPStaging::getWPpath();
292
+ // }
293
+ // return trailingslashit( $this->options->cloneDir );
294
+ // }
295
+
296
+ /**
297
+ * Adapated from interconnect/it's search/replace script, adapted from Better Search Replace
298
+ *
299
+ * Modified to use WordPress wpdb functions instead of PHP's native mysql/pdo functions,
300
+ * and to be compatible with batch processing.
301
+ *
302
+ * @link https://interconnectit.com/products/search-and-replace-for-wordpress-databases/
303
+ *
304
+ * @access public
305
+ * @param string $table The table to run the replacement on.
306
+ * @param int $page The page/block to begin the query on.
307
+ * @param array $args An associative array containing arguments for this run.
308
+ * @return array
309
+ */
310
+ private function searchReplace( $table, $page, $args ) {
311
+
312
+ if( $this->thirdParty->isSearchReplaceExcluded( $table ) ) {
313
+ $this->log( "DB Processing: Skip {$table}", \WPStaging\Utils\Logger::TYPE_INFO );
314
+ return true;
315
+ }
316
+
317
+ // Load up the default settings for this chunk.
318
+ $table = esc_sql( $table );
319
+ $current_page = $this->options->job->start + $this->settings->querySRLimit;
320
+ $pages = $this->get_pages_in_table( $table );
321
+
322
+
323
+ // Search URL example.com/staging and root path to staging site /var/www/htdocs/staging
324
+ $args['search_for'] = array(
325
+ '//' . $this->sourceHostname,
326
+ ABSPATH,
327
+ '\/\/' . str_replace( '/', '\/', $this->sourceHostname ), // // Used by revslider and several visual editors
328
+ $this->getImagePathLive()
329
+ );
330
+
331
+
332
+ $args['replace_with'] = array(
333
+ '//' . $this->destinationHostname,
334
+ $this->options->destinationDir,
335
+ '\/\/' . str_replace( '/', '\/', $this->destinationHostname ), // Used by revslider and several visual editors
336
+ $this->getImagePathStaging()
337
+ );
338
+
339
+ // if( $this->isSubDir() ) {
340
+ // // Search URL example.com/staging and root path to staging site /var/www/htdocs/staging
341
+ // $args['search_for'] = array(
342
+ // '//' . rtrim( $this->multisiteHomeUrlWithoutScheme, "/" ) . $this->getSubDir(),
343
+ // ABSPATH,
344
+ // str_replace( '/', '\/', rtrim( $this->multisiteHomeUrlWithoutScheme, '/' ) ) . str_replace( '/', '\/', $this->getSubDir() ), // // Used by revslider and several visual editors
345
+ // $this->getImagePathLive()
346
+ // );
347
+ //
348
+ //
349
+ // $args['replace_with'] = array(
350
+ // '//' . rtrim( $this->multisiteDomainWithoutScheme, "/" ) . $this->getSubDir() . '/' . $this->options->cloneDirectoryName,
351
+ // rtrim( $this->getAbsDestination(), '/' ) . '/' . $this->options->cloneDirectoryName,
352
+ // str_replace( '/', '\/', rtrim( $this->multisiteDomainWithoutScheme, "/" ) ) . str_replace( '/', '\/', $this->getSubDir() ) . '\/' . $this->options->cloneDirectoryName, // Used by revslider and several visual editors
353
+ // $this->getImagePathStaging()
354
+ // );
355
+ // } else {
356
+ // $args['search_for'] = array(
357
+ // '//' . rtrim( $this->multisiteHomeUrlWithoutScheme, '/' ),
358
+ // ABSPATH,
359
+ // str_replace( '/', '\/', rtrim( $this->multisiteHomeUrlWithoutScheme, '/' ) ),
360
+ // $this->getImagePathLive()
361
+ // );
362
+ // $args['replace_with'] = array(
363
+ // '//' . rtrim( $this->multisiteDomainWithoutScheme, '/' ) . '/' . $this->options->cloneDirectoryName,
364
+ // rtrim( $this->getAbsDestination(), '/' ) . '/' . $this->options->cloneDirectoryName,
365
+ // str_replace( '/', '\/', rtrim( $this->multisiteDomainWithoutScheme, '/' ) ) . '\/' . $this->options->cloneDirectoryName,
366
+ // $this->getImagePathStaging()
367
+ // );
368
+ // }
369
+
370
+ $args['replace_guids'] = 'off';
371
+ $args['dry_run'] = 'off';
372
+ $args['case_insensitive'] = false;
373
+ $args['replace_mails'] = 'off';
374
+ $args['skip_transients'] = 'on';
375
+
376
+
377
+ // Allow filtering of search & replace parameters
378
+ $args = apply_filters( 'wpstg_clone_searchreplace_params', $args );
379
+
380
+ // Get a list of columns in this table.
381
+ list( $primary_key, $columns ) = $this->get_columns( $table );
382
+
383
+ // Bail out early if there isn't a primary key.
384
+ // We commented this to search & replace through tables which have no primary keys like wp_revslider_slides
385
+ // @todo test this carefully. If it causes (performance) issues we need to activate it again!
386
+ // @since 2.4.4
387
+ // if( null === $primary_key ) {
388
+ // return false;
389
+ // }
390
+
391
+ $current_row = 0;
392
+ $start = $this->options->job->start;
393
+ $end = $this->settings->querySRLimit;
394
+
395
+ // Grab the content of the table.
396
+ $data = $this->stagingDb->get_results( "SELECT * FROM $table LIMIT $start, $end", ARRAY_A );
397
+
398
+ // Filter certain rows (of other plugins)
399
+ $filter = array(
400
+ 'Admin_custome_login_Slidshow',
401
+ 'Admin_custome_login_Social',
402
+ 'Admin_custome_login_logo',
403
+ 'Admin_custome_login_text',
404
+ 'Admin_custome_login_login',
405
+ 'Admin_custome_login_top',
406
+ 'Admin_custome_login_dashboard',
407
+ 'Admin_custome_login_Version',
408
+ 'upload_path',
409
+ );
410
+
411
+ $filter = apply_filters( 'wpstg_clone_searchreplace_excl_rows', $filter );
412
+
413
+ // Loop through the data.
414
+ foreach ( $data as $row ) {
415
+ $current_row++;
416
+ $update_sql = array();
417
+ $where_sql = array();
418
+ $upd = false;
419
+
420
+ // Skip rows below
421
+ if( isset( $row['option_name'] ) && in_array( $row['option_name'], $filter ) ) {
422
+ continue;
423
+ }
424
+
425
+ // Skip rows with transients (They can store huge data and we need to save memory)
426
+ if( isset( $row['option_name'] ) && 'on' === $args['skip_transients'] && false !== strpos( $row['option_name'], '_transient' ) ) {
427
+ continue;
428
+ }
429
+
430
+ foreach ( $columns as $column ) {
431
+
432
+ $dataRow = $row[$column];
433
+
434
+ if( $column == $primary_key ) {
435
+ $where_sql[] = $column . ' = "' . $this->mysql_escape_mimic( $dataRow ) . '"';
436
+ continue;
437
+ }
438
+
439
+ // Skip GUIDs by default.
440
+ if( 'on' !== $args['replace_guids'] && 'guid' == $column ) {
441
+ continue;
442
+ }
443
+
444
+ // Skip mail addresses
445
+ if( 'off' === $args['replace_mails'] && false !== strpos( $dataRow, '@' . $this->multisiteDomainWithoutScheme ) ) {
446
+ continue;
447
+ }
448
+
449
+ // Check options table
450
+ if( $this->options->prefix . 'options' === $table ) {
451
+
452
+ // Skip certain options
453
+ if( isset( $should_skip ) && true === $should_skip ) {
454
+ $should_skip = false;
455
+ continue;
456
+ }
457
+
458
+ // Skip this row
459
+ if( 'wpstg_existing_clones_beta' === $dataRow ||
460
+ 'wpstg_existing_clones' === $dataRow ||
461
+ 'wpstg_settings' === $dataRow ||
462
+ 'wpstg_license_status' === $dataRow ||
463
+ 'siteurl' === $dataRow ||
464
+ 'home' === $dataRow
465
+ ) {
466
+ $should_skip = true;
467
+ }
468
+ }
469
+
470
+ // Check the path delimiter for / or \/ and remove one of those which prevents from resulting in wrong syntax like domain.com/staging\/.
471
+ // 1. local.wordpress.test -> local.wordpress.test/staging
472
+ // 2. local.wordpress.test\/ -> local.wordpress.test\/staging\/
473
+ $tmp = $args;
474
+ if( false === strpos( $dataRow, $tmp['search_for'][0] ) ) {
475
+ array_shift( $tmp['search_for'] ); // rtrim( $this->homeUrl, '/' ),
476
+ array_shift( $tmp['replace_with'] ); // rtrim( $this->homeUrl, '/' ) . '/' . $this->options->cloneDirectoryName,
477
+ } else {
478
+ unset( $tmp['search_for'][1] );
479
+ unset( $tmp['replace_with'][1] );
480
+ // recount array
481
+ $tmp['search_for'] = array_values( $tmp['search_for'] );
482
+ $tmp['replace_with'] = array_values( $tmp['replace_with'] );
483
+ }
484
+
485
+ // Run a search replace on the data row and respect the serialisation.
486
+ $i = 0;
487
+ foreach ( $tmp['search_for'] as $replace ) {
488
+ $dataRow = $this->recursive_unserialize_replace( $tmp['search_for'][$i], $tmp['replace_with'][$i], $dataRow, false, $args['case_insensitive'] );
489
+ $i++;
490
+ }
491
+ unset( $replace );
492
+ unset( $i );
493
+ unset( $tmp );
494
+
495
+ // Something was changed
496
+ if( $row[$column] != $dataRow ) {
497
+ $update_sql[] = $column . ' = "' . $this->mysql_escape_mimic( $dataRow ) . '"';
498
+ $upd = true;
499
+ }
500
+ }
501
+
502
+ // Determine what to do with updates.
503
+ if( $args['dry_run'] === 'on' ) {
504
+ // Don't do anything if a dry run
505
+ } elseif( $upd && !empty( $where_sql ) ) {
506
+ // If there are changes to make, run the query.
507
+ $sql = 'UPDATE ' . $table . ' SET ' . implode( ', ', $update_sql ) . ' WHERE ' . implode( ' AND ', array_filter( $where_sql ) );
508
+ $result = $this->stagingDb->query( $sql );
509
+
510
+ if( !$result ) {
511
+ $this->log( "Error updating row {$current_row} SQL: {$sql}", \WPStaging\Utils\Logger::TYPE_ERROR );
512
+ }
513
+ }
514
+ } // end row loop
515
+ unset( $row );
516
+ unset( $update_sql );
517
+ unset( $where_sql );
518
+ unset( $sql );
519
+
520
+
521
+ // DB Flush
522
+ $this->stagingDb->flush();
523
+ return true;
524
+ }
525
+
526
+ /**
527
+ * Get path to multisite image folder e.g. wp-content/blogs.dir/ID/files or wp-content/uploads/sites/ID
528
+ * @return string
529
+ */
530
+ private function getImagePathLive() {
531
+ // Check first which structure is used
532
+ $uploads = wp_upload_dir();
533
+ $basedir = $uploads['basedir'];
534
+ $blogId = get_current_blog_id();
535
+
536
+ if( false === strpos( $basedir, 'blogs.dir' ) ) {
537
+ // Since WP 3.5
538
+ $path = $blogId > 1 ?
539
+ 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'sites' . DIRECTORY_SEPARATOR . get_current_blog_id() . DIRECTORY_SEPARATOR :
540
+ 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR;
541
+ } else {
542
+ // old blog structure
543
+ $path = $blogId > 1 ?
544
+ 'wp-content' . DIRECTORY_SEPARATOR . 'blogs.dir' . DIRECTORY_SEPARATOR . get_current_blog_id() . DIRECTORY_SEPARATOR . 'files' . DIRECTORY_SEPARATOR :
545
+ 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR;
546
+ }
547
+ return $path;
548
+ }
549
+
550
+ /**
551
+ * Get path to staging site image path wp-content/uploads
552
+ * @return string
553
+ */
554
+ private function getImagePathStaging() {
555
+ return 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR;
556
+ }
557
+
558
+ /**
559
+ * Adapted from interconnect/it's search/replace script.
560
+ *
561
+ * @link https://interconnectit.com/products/search-and-replace-for-wordpress-databases/
562
+ *
563
+ * Take a serialised array and unserialise it replacing elements as needed and
564
+ * unserialising any subordinate arrays and performing the replace on those too.
565
+ *
566
+ * @access private
567
+ * @param string $from String we're looking to replace.
568
+ * @param string $to What we want it to be replaced with
569
+ * @param array $data Used to pass any subordinate arrays back to in.
570
+ * @param boolean $serialized Does the array passed via $data need serialising.
571
+ * @param sting|boolean $case_insensitive Set to 'on' if we should ignore case, false otherwise.
572
+ *
573
+ * @return string|array The original array with all elements replaced as needed.
574
+ */
575
+ private function recursive_unserialize_replace( $from = '', $to = '', $data = '', $serialized = false, $case_insensitive = false ) {
576
+ try {
577
+ // Some unserialized data cannot be re-serialized eg. SimpleXMLElements
578
+ if( is_serialized( $data ) && ( $unserialized = @unserialize( $data ) ) !== false ) {
579
+ $data = $this->recursive_unserialize_replace( $from, $to, $unserialized, true, $case_insensitive );
580
+ } elseif( is_array( $data ) ) {
581
+ $tmp = array();
582
+ foreach ( $data as $key => $value ) {
583
+ $tmp[$key] = $this->recursive_unserialize_replace( $from, $to, $value, false, $case_insensitive );
584
+ }
585
+
586
+ $data = $tmp;
587
+ unset( $tmp );
588
+ } elseif( is_object( $data ) ) {
589
+ $tmp = $data;
590
+ $props = get_object_vars( $data );
591
+ foreach ( $props as $key => $value ) {
592
+ if( $key === '' || ord( $key[0] ) === 0 ) {
593
+ continue;
594
+ }
595
+ $tmp->$key = $this->recursive_unserialize_replace( $from, $to, $value, false, $case_insensitive );
596
+ }
597
+
598
+ $data = $tmp;
599
+ unset( $tmp );
600
+ } else {
601
+ if( is_string( $data ) ) {
602
+ if( !empty( $from ) && !empty( $to ) ) {
603
+ $data = $this->str_replace( $from, $to, $data, $case_insensitive );
604
+ }
605
+ }
606
+ }
607
+
608
+ if( $serialized ) {
609
+ return serialize( $data );
610
+ }
611
+ } catch ( Exception $error ) {
612
+
613
+ }
614
+
615
+ return $data;
616
+ }
617
+
618
+ /**
619
+ * Check if the object is a valid one and not __PHP_Incomplete_Class_Name
620
+ * Can not use is_object alone because in php 7.2 it's returning true even though object is __PHP_Incomplete_Class_Name
621
+ * @return boolean
622
+ */
623
+ // private function isValidObject( $data ) {
624
+ // if( !is_object( $data ) || gettype( $data ) != 'object' ) {
625
+ // return false;
626
+ // }
627
+ //
628
+ // $invalid_class_props = get_object_vars( $data );
629
+ //
630
+ // if (!isset($invalid_class_props['__PHP_Incomplete_Class_Name'])){
631
+ // // Assume it must be an valid object
632
+ // return true;
633
+ // }
634
+ //
635
+ // $invalid_object_class = $invalid_class_props['__PHP_Incomplete_Class_Name'];
636
+ //
637
+ // if( !empty( $invalid_object_class ) ) {
638
+ // return false;
639
+ // }
640
+ //
641
+ // // Assume it must be an valid object
642
+ // return true;
643
+ // }
644
+
645
+ /**
646
+ * Mimics the mysql_real_escape_string function. Adapted from a post by 'feedr' on php.net.
647
+ * @link http://php.net/manual/en/function.mysql-real-escape-string.php#101248
648
+ * @access public
649
+ * @param string $input The string to escape.
650
+ * @return string
651
+ */
652
+ private function mysql_escape_mimic( $input ) {
653
+ if( is_array( $input ) ) {
654
+ return array_map( __METHOD__, $input );
655
+ }
656
+ if( !empty( $input ) && is_string( $input ) ) {
657
+ return str_replace( array('\\', "\0", "\n", "\r", "'", '"', "\x1a"), array('\\\\', '\\0', '\\n', '\\r', "\\'", '\\"', '\\Z'), $input );
658
+ }
659
+
660
+ return $input;
661
+ }
662
+
663
+ /**
664
+ * Return unserialized object or array
665
+ *
666
+ * @param string $serialized_string Serialized string.
667
+ * @param string $method The name of the caller method.
668
+ *
669
+ * @return mixed, false on failure
670
+ */
671
+ private static function unserialize( $serialized_string ) {
672
+ if( !is_serialized( $serialized_string ) ) {
673
+ return false;
674
+ }
675
+
676
+ $serialized_string = trim( $serialized_string );
677
+ $unserialized_string = @unserialize( $serialized_string );
678
+
679
+ return $unserialized_string;
680
+ }
681
+
682
+ /**
683
+ * Wrapper for str_replace
684
+ *
685
+ * @param string $from
686
+ * @param string $to
687
+ * @param string $data
688
+ * @param string|bool $case_insensitive
689
+ *
690
+ * @return string
691
+ */
692
+ private function str_replace( $from, $to, $data, $case_insensitive = false ) {
693
+
694
+ // Add filter
695
+ $excludes = apply_filters( 'wpstg_clone_searchreplace_excl', array() );
696
+
697
+ // Build pattern
698
+ $regexExclude = '';
699
+ foreach ( $excludes as $exclude ) {
700
+ $regexExclude .= $exclude . '(*SKIP)(FAIL)|';
701
+ }
702
+
703
+ if( 'on' === $case_insensitive ) {
704
+ //$data = str_ireplace( $from, $to, $data );
705
+ $data = preg_replace( '#' . $regexExclude . preg_quote( $from ) . '#i', $to, $data );
706
+ } else {
707
+ //$data = str_replace( $from, $to, $data );
708
+ $data = preg_replace( '#' . $regexExclude . preg_quote( $from ) . '#', $to, $data );
709
+ }
710
+
711
+ return $data;
712
+ }
713
+
714
+ /**
715
+ * Set the job
716
+ * @param string $table
717
+ */
718
+ private function setJob( $table ) {
719
+ if( !empty( $this->options->job->current ) ) {
720
+ return;
721
+ }
722
+
723
+ $this->options->job->current = $table;
724
+ $this->options->job->start = 0;
725
+ }
726
+
727
+ /**
728
+ * Start Job
729
+ * @param string $new
730
+ * @param string $old
731
+ * @return bool
732
+ */
733
+ private function startJob( $new, $old ) {
734
+
735
+ if( $this->isExcludedTable( $new ) ) {
736
+ return false;
737
+ }
738
+
739
+ // Table does not exists
740
+ $result = $this->productionDb->query( "SHOW TABLES LIKE '{$old}'" );
741
+ if( !$result || 0 === $result ) {
742
+ return false;
743
+ }
744
+
745
+ if( 0 != $this->options->job->start ) {
746
+ return true;
747
+ }
748
+
749
+ $this->options->job->total = ( int ) $this->productionDb->get_var( "SELECT COUNT(1) FROM {$old}" );
750
+
751
+ if( 0 == $this->options->job->total ) {
752
+ $this->finishStep();
753
+ return false;
754
+ }
755
+
756
+ return true;
757
+ }
758
+
759
+ /**
760
+ * Is table excluded from search replace processing?
761
+ * @param string $table
762
+ * @return boolean
763
+ */
764
+ private function isExcludedTable( $table ) {
765
+
766
+ $customTables = apply_filters( 'wpstg_clone_searchreplace_tables_exclude', array() );
767
+ $defaultTables = array('blogs');
768
+
769
+ $tables = array_merge( $customTables, $defaultTables );
770
+
771
+ $excludedTables = array();
772
+ foreach ( $tables as $key => $value ) {
773
+ $excludedTables[] = $this->options->prefix . $value;
774
+ }
775
+
776
+ if( in_array( $table, $excludedTables ) ) {
777
+ return true;
778
+ }
779
+ return false;
780
+ }
781
+
782
+ /**
783
+ * Finish the step
784
+ */
785
+ private function finishStep() {
786
+ // This job is not finished yet
787
+ if( $this->options->job->total > $this->options->job->start ) {
788
+ return false;
789
+ }
790
+
791
+ // Add it to cloned tables listing
792
+ $this->options->clonedTables[] = $this->options->tables[$this->options->currentStep];
793
+
794
+ // Reset job
795
+ $this->options->job = new \stdClass();
796
+
797
+ return true;
798
+ }
799
+
800
+ /**
801
+ * Drop table if necessary
802
+ * @param string $new
803
+ */
804
+ private function dropTable( $new ) {
805
+ $old = $this->stagingDb->get_var( $this->stagingDb->prepare( "SHOW TABLES LIKE %s", $new ) );
806
+
807
+ if( !$this->shouldDropTable( $new, $old ) ) {
808
+ return;
809
+ }
810
+
811
+ $this->log( "DB Processing: {$new} already exists, dropping it first" );
812
+ $this->stagingDb->query( "DROP TABLE {$new}" );
813
+ }
814
+
815
+ /**
816
+ * Check if table needs to be dropped
817
+ * @param string $new
818
+ * @param string $old
819
+ * @return bool
820
+ */
821
+ private function shouldDropTable( $new, $old ) {
822
+ return (
823
+ $old == $new &&
824
+ (
825
+ !isset( $this->options->job->current ) ||
826
+ !isset( $this->options->job->start ) ||
827
+ 0 == $this->options->job->start
828
+ )
829
+ );
830
+ }
831
+
832
+ /**
833
+ * Check if WP is installed in subdir
834
+ * @return boolean
835
+ */
836
+ private function isSubDir() {
837
+ // Compare names without scheme to bypass cases where siteurl and home have different schemes http / https
838
+ // This is happening much more often than you would expect
839
+ $siteurl = preg_replace( '#^https?://#', '', rtrim( get_option( 'siteurl' ), '/' ) );
840
+ $home = preg_replace( '#^https?://#', '', rtrim( get_option( 'home' ), '/' ) );
841
+
842
+ if( $home !== $siteurl ) {
843
+ return true;
844
+ }
845
+ return false;
846
+ }
847
+
848
+ }
apps/Backend/Modules/Jobs/SearchReplace.php CHANGED
@@ -308,7 +308,7 @@ class SearchReplace extends JobExecutable {
308
  $args['replace_with'] = array(
309
  '\/\/' . str_replace( '/', '\/', $this->destinationHostname ),
310
  '//' . $this->destinationHostname,
311
- $this->options->destinationDir,
312
  );
313
 
314
 
308
  $args['replace_with'] = array(
309
  '\/\/' . str_replace( '/', '\/', $this->destinationHostname ),
310
  '//' . $this->destinationHostname,
311
+ rtrim ($this->options->destinationDir, '/'),
312
  );
313
 
314
 
apps/Backend/Modules/Jobs/Updating.php CHANGED
@@ -86,8 +86,8 @@ class Updating extends Job {
86
  $this->options->databasePrefix = $this->options->existingClones[$this->options->clone]['databasePrefix'];
87
  $this->options->destinationHostname = $this->options->existingClones[$this->options->clone]['url'];
88
  $this->options->prefix = $this->getStagingPrefix();
89
- $helper = new Helper();
90
- $this->options->homeHostname = $helper->get_home_url_without_scheme();
91
  } else {
92
  wp_die( 'Fatal Error: Can not update clone because there is no clone data.' );
93
  }
@@ -138,6 +138,9 @@ class Updating extends Job {
138
  if( isset( $_POST["cloneHostname"] ) && !empty( $_POST["cloneHostname"] ) ) {
139
  $this->options->cloneHostname = $_POST["cloneHostname"];
140
  }
 
 
 
141
 
142
  // Directories to Copy
143
  $this->options->directoriesToCopy = array_merge(
@@ -149,6 +152,18 @@ class Updating extends Job {
149
  return $this->saveOptions();
150
  }
151
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  /**
153
  * Get Destination Directory including staging subdirectory
154
  * @return type
86
  $this->options->databasePrefix = $this->options->existingClones[$this->options->clone]['databasePrefix'];
87
  $this->options->destinationHostname = $this->options->existingClones[$this->options->clone]['url'];
88
  $this->options->prefix = $this->getStagingPrefix();
89
+ $helper = new Helper();
90
+ $this->options->homeHostname = $helper->get_home_url_without_scheme();
91
  } else {
92
  wp_die( 'Fatal Error: Can not update clone because there is no clone data.' );
93
  }
138
  if( isset( $_POST["cloneHostname"] ) && !empty( $_POST["cloneHostname"] ) ) {
139
  $this->options->cloneHostname = $_POST["cloneHostname"];
140
  }
141
+
142
+ $this->options->destinationHostname = $this->getDestinationHostname();
143
+
144
 
145
  // Directories to Copy
146
  $this->options->directoriesToCopy = array_merge(
152
  return $this->saveOptions();
153
  }
154
 
155
+ /**
156
+ * Return target hostname
157
+ * @return string
158
+ */
159
+ private function getDestinationHostname() {
160
+ if( empty( $this->options->cloneHostname ) ) {
161
+ $helper = new Helper();
162
+ return $helper->get_home_url_without_scheme();
163
+ }
164
+ return $this->getHostnameWithoutScheme( $this->options->cloneHostname );
165
+ }
166
+
167
  /**
168
  * Get Destination Directory including staging subdirectory
169
  * @return type
apps/Backend/Modules/Views/Forms/Settings.php CHANGED
@@ -59,7 +59,7 @@ class Settings {
59
 
60
  $this->form["general"]->add(
61
  $element->setLabel( "DB Copy Query Limit" )
62
- ->setDefault( isset( $settings->queryLimit ) ? $settings->queryLimit : 10000 )
63
  );
64
  // DB Search & Replace Query Limit
65
  $element = new Numerical(
@@ -73,7 +73,7 @@ class Settings {
73
 
74
  $this->form["general"]->add(
75
  $element->setLabel( "DB Search & Replace Limit" )
76
- ->setDefault( isset( $settings->querySRLimit ) ? $settings->querySRLimit : 5000 )
77
  );
78
 
79
  $options = array('1' => '1', '10' => '10', '50' => '50', '250' => '250', '500' => '500', '1000' => '1000');
@@ -136,6 +136,21 @@ class Settings {
136
  ->setDefault( isset( $settings->cpuLoad ) ? $settings->cpuLoad : "low" )
137
  );
138
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
 
140
  // Optimizer
141
  $element = new Check(
@@ -144,7 +159,7 @@ class Settings {
144
 
145
  $this->form["general"]->add(
146
  $element->setLabel( "Optimizer" )
147
- ->setDefault( (isset( $settings->optimizer )) ? $settings->optimizer : null )
148
  );
149
 
150
  // Plugins
59
 
60
  $this->form["general"]->add(
61
  $element->setLabel( "DB Copy Query Limit" )
62
+ ->setDefault( isset( $settings->queryLimit ) ? $settings->queryLimit : 10000 )
63
  );
64
  // DB Search & Replace Query Limit
65
  $element = new Numerical(
73
 
74
  $this->form["general"]->add(
75
  $element->setLabel( "DB Search & Replace Limit" )
76
+ ->setDefault( isset( $settings->querySRLimit ) ? $settings->querySRLimit : 5000 )
77
  );
78
 
79
  $options = array('1' => '1', '10' => '10', '50' => '50', '250' => '250', '500' => '500', '1000' => '1000');
136
  ->setDefault( isset( $settings->cpuLoad ) ? $settings->cpuLoad : "low" )
137
  );
138
 
139
+ // Delay Between Requests
140
+ $element = new Numerical(
141
+ "wpstg_settings[delayRequests]", array(
142
+ "class" => "medium-text",
143
+ "step" => 1,
144
+ "max" => 5,
145
+ "min" => 0
146
+ )
147
+ );
148
+
149
+ $this->form["general"]->add(
150
+ $element->setLabel( "Delay Between Requests" )
151
+ ->setDefault( (isset( $settings->delayRequests )) ? $settings->delayRequests : 0 )
152
+ );
153
+
154
 
155
  // Optimizer
156
  $element = new Check(
159
 
160
  $this->form["general"]->add(
161
  $element->setLabel( "Optimizer" )
162
+ ->setDefault( (isset( $settings->optimizer )) ? $settings->optimizer : null )
163
  );
164
 
165
  // Plugins
apps/Core/WPStaging.php CHANGED
@@ -29,7 +29,7 @@ final class WPStaging {
29
  /**
30
  * Plugin version
31
  */
32
- const VERSION = "2.4.0";
33
 
34
  /**
35
  * Plugin name
29
  /**
30
  * Plugin version
31
  */
32
+ const VERSION = "2.4.1";
33
 
34
  /**
35
  * Plugin name
readme.txt CHANGED
@@ -9,7 +9,7 @@ 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: 4.9
12
- Stable tag: 2.4.0
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.
@@ -146,6 +146,11 @@ https://wp-staging.com
146
 
147
  == Changelog ==
148
 
 
 
 
 
 
149
  = 2.4.0 =
150
  * New: Compatible to WP 5.0.0 Gutenberg
151
  * New: Increase file scanning process performance
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: 4.9
12
+ Stable tag: 2.4.1
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.
146
 
147
  == Changelog ==
148
 
149
+ = 2.4.1 =
150
+ * Fix: Error notice if user tries to update staging site located in another database
151
+ * Fix: Search & Replace adds staging site subfolder multiple times to replaced string when staging site is updated
152
+ * Fix: Missing ajax delay setting
153
+
154
  = 2.4.0 =
155
  * New: Compatible to WP 5.0.0 Gutenberg
156
  * New: Increase file scanning process performance
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.4.0
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.4.0' );
55
  }
56
 
57
  /**
7
  * Author: WP-Staging
8
  * Author URI: https://wp-staging.com
9
  * Contributors: ReneHermi, ilgityildirim
10
+ * Version: 2.4.1
11
  * Text Domain: wp-staging
12
  * Domain Path: /languages/
13
 
51
 
52
  // Version
53
  if( !defined( 'WPSTG_VERSION' ) ) {
54
+ define( 'WPSTG_VERSION', '2.4.1' );
55
  }
56
 
57
  /**