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

Version Description

  • New: Improve styling of login form. Thanks to Andy Kennan (Screaming Frog)
  • New: Add 'password lost' button to login form
  • New: Change welcome page CTA
  • New: Add feedback option when plugin is disabled
  • Fix: PDO instances can not be serialized or unserialized
  • Fix: Can not update staging site db table if there are constraints in it
Download this release

Release Info

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

Code changes from version 2.5.9 to 2.6.1

apps/Backend/Administrator.php CHANGED
@@ -23,6 +23,7 @@ use WPStaging\DI\InjectionAware;
23
use WPStaging\Backend\Modules\Views\Forms\Settings as FormSettings;
24
use WPStaging\Utils\Report;
25
use WPStaging\Backend\Activation;
26
27
/**
28
* Class Administrator
@@ -84,6 +85,11 @@ class Administrator extends InjectionAware {
84
$loader->addAction( "admin_post_wpstg_import_settings", $this, "import" );
85
$loader->addAction( "admin_notices", $this, "messages" );
86
87
// Ajax Requests
88
$loader->addAction( "wp_ajax_wpstg_overview", $this, "ajaxOverview" );
89
$loader->addAction( "wp_ajax_wpstg_scanning", $this, "ajaxScan" );
@@ -108,12 +114,30 @@ class Administrator extends InjectionAware {
108
$loader->addAction( "wp_ajax_wpstg_logs", $this, "ajaxLogs" );
109
$loader->addAction( "wp_ajax_wpstg_check_disk_space", $this, "ajaxCheckFreeSpace" );
110
$loader->addAction( "wp_ajax_wpstg_send_report", $this, "ajaxSendReport" );
111
112
// Ajax hooks pro Version
113
$loader->addAction( "wp_ajax_wpstg_scan", $this, "ajaxPushScan" );
114
$loader->addAction( "wp_ajax_wpstg_push_processing", $this, "ajaxPushProcessing" );
115
}
116
117
/**
118
* Register options form elements
119
*/
@@ -636,8 +660,8 @@ class Administrator extends InjectionAware {
636
* @return mixed bool | json
637
*/
638
public function ajaxHideLaterRating() {
639
- $date = date('Y-m-d', strtotime(date('Y-m-d'). ' + 7 days'));
640
- if( false !== update_option( 'wpstg_rating',$date )) {
641
wp_send_json( true );
642
}
643
return wp_send_json( false );
@@ -792,8 +816,8 @@ class Administrator extends InjectionAware {
792
// }
793
$db->select( $database );
794
if( !$db->ready ) {
795
- $error = isset($db->error->errors['db_select_fail']) ? $db->error->errors['db_select_fail'] : "Error: Can't select {database} Either it does not exist or you don't have privileges to access it.";
796
- echo json_encode( array('errors' => $error ) );
797
exit;
798
}
799
echo json_encode( array('success' => 'true') );
23
use WPStaging\Backend\Modules\Views\Forms\Settings as FormSettings;
24
use WPStaging\Utils\Report;
25
use WPStaging\Backend\Activation;
26
+ use WPStaging\Backend\Feedback;
27
28
/**
29
* Class Administrator
85
$loader->addAction( "admin_post_wpstg_import_settings", $this, "import" );
86
$loader->addAction( "admin_notices", $this, "messages" );
87
88
+ //require_once WPSTG_PLUGIN_DIR . 'apps/Backend/Feedback/Feedback.php';
89
+ //add_filter( 'admin_footer', 'mashsb_add_deactivation_feedback_modal' );
90
+ add_filter( 'admin_footer', array($this, 'loadFeedbackForm') );
91
+
92
+
93
// Ajax Requests
94
$loader->addAction( "wp_ajax_wpstg_overview", $this, "ajaxOverview" );
95
$loader->addAction( "wp_ajax_wpstg_scanning", $this, "ajaxScan" );
114
$loader->addAction( "wp_ajax_wpstg_logs", $this, "ajaxLogs" );
115
$loader->addAction( "wp_ajax_wpstg_check_disk_space", $this, "ajaxCheckFreeSpace" );
116
$loader->addAction( "wp_ajax_wpstg_send_report", $this, "ajaxSendReport" );
117
+ $loader->addAction( "wp_ajax_wpstg_send_feedback", $this, "sendFeedback" );
118
+
119
120
// Ajax hooks pro Version
121
$loader->addAction( "wp_ajax_wpstg_scan", $this, "ajaxPushScan" );
122
$loader->addAction( "wp_ajax_wpstg_push_processing", $this, "ajaxPushProcessing" );
123
}
124
125
+ /**
126
+ * Load Feedback Form on plugins.php
127
+ */
128
+ public function loadFeedbackForm() {
129
+ $form = new Feedback\Feedback();
130
+ $load = $form->loadForm();
131
+ }
132
+
133
+ /**
134
+ * Send Feedback form via mail
135
+ */
136
+ public function sendFeedback() {
137
+ $form = new Feedback\Feedback();
138
+ $send = $form->sendMail();
139
+ }
140
+
141
/**
142
* Register options form elements
143
*/
660
* @return mixed bool | json
661
*/
662
public function ajaxHideLaterRating() {
663
+ $date = date( 'Y-m-d', strtotime( date( 'Y-m-d' ) . ' + 7 days' ) );
664
+ if( false !== update_option( 'wpstg_rating', $date ) ) {
665
wp_send_json( true );
666
}
667
return wp_send_json( false );
816
// }
817
$db->select( $database );
818
if( !$db->ready ) {
819
+ $error = isset( $db->error->errors['db_select_fail'] ) ? $db->error->errors['db_select_fail'] : "Error: Can't select {database} Either it does not exist or you don't have privileges to access it.";
820
+ echo json_encode( array('errors' => $error) );
821
exit;
822
}
823
echo json_encode( array('success' => 'true') );
apps/Backend/Feedback/Feedback.php ADDED
@@ -0,0 +1,149 @@
1
+ <?php
2
+
3
+ namespace WPStaging\Backend\Feedback;
4
+
5
+ class Feedback {
6
+ // public function __construct() {
7
+ //
8
+ // }
9
+
10
+ /**
11
+ * Current page is plugins.php
12
+ * @global array $pagenow
13
+ * @return bool
14
+ */
15
+ private function isPluginsPage() {
16
+ global $pagenow;
17
+ return ( 'plugins.php' === $pagenow );
18
+ }
19
+
20
+ /**
21
+ * Load feedback form
22
+ * @return string
23
+ */
24
+ public function loadForm() {
25
+
26
+ $screen = get_current_screen();
27
+ if( !is_admin() && !$this->isPluginsPage() ) {
28
+ return;
29
+ }
30
+
31
+ $current_user = wp_get_current_user();
32
+ if( !($current_user instanceof WP_User) ) {
33
+ $email = '';
34
+ } else {
35
+ $email = trim( $current_user->user_email );
36
+ }
37
+
38
+ include WPSTG_PLUGIN_DIR . 'apps/Backend/views/feedback/deactivate-feedback.php';
39
+ }
40
+
41
+ public function sendMail() {
42
+
43
+ if( isset( $_POST['data'] ) ) {
44
+ parse_str( $_POST['data'], $form );
45
+ }
46
+
47
+ $text = '';
48
+ if( isset( $form['wpstg_disable_text'] ) ) {
49
+ $text = implode( "\n\r", $form['wpstg_disable_text'] );
50
+ }
51
+
52
+ $headers = array();
53
+
54
+ $from = isset( $form['wpstg_disable_from'] ) ? $form['wpstg_disable_from'] : '';
55
+ if( $from ) {
56
+ $headers[] = "From: $from";
57
+ $headers[] = "Reply-To: $from";
58
+ }
59
+
60
+ $subject = isset( $form['wpstg_disable_reason'] ) ? 'WP Staging Free: '. $form['wpstg_disable_reason'] : 'WP Staging Free: (no reason given)';
61
+
62
+ $success = wp_mail( 'feedback@wp-staging.com', $subject, $text, $headers );
63
+
64
+ //error_log(print_r($success, true));
65
+ //error_log($from . $subject . var_dump($form));
66
+
67
+ if( $success ) {
68
+ wp_die( 1 );
69
+ }
70
+ wp_die( 0 );
71
+ }
72
+
73
+ }
74
+
75
+ /**
76
+ * Helper method to check if user is in the plugins page.
77
+ *
78
+ * @author René Hermenau
79
+ * @since 3.3.7
80
+ *
81
+ * @return bool
82
+ */
83
+ //function mashsb_is_plugins_page() {
84
+ // global $pagenow;
85
+ //
86
+ // return ( 'plugins.php' === $pagenow );
87
+ //}
88
+
89
+ /**
90
+ * display deactivation logic on plugins page
91
+ *
92
+ * @since 3.3.7
93
+ */
94
+ //function mashsb_add_deactivation_feedback_modal() {
95
+ //
96
+ // $screen = get_current_screen();
97
+ // if( !is_admin() && !mashsb_is_plugins_page() ) {
98
+ // return;
99
+ // }
100
+ //
101
+ // $current_user = wp_get_current_user();
102
+ // if( !($current_user instanceof WP_User) ) {
103
+ // $email = '';
104
+ // } else {
105
+ // $email = trim( $current_user->user_email );
106
+ // }
107
+ //
108
+ // include WPSTG_PLUGIN_DIR . 'apps/Backend/views/feedback/deactivate-feedback.php';
109
+ //}
110
+
111
+ /**
112
+ * send feedback via email
113
+ *
114
+ * @since 1.4.0
115
+ */
116
+ //function wpstg_send_feedback() {
117
+ //
118
+ // if( isset( $_POST['data'] ) ) {
119
+ // parse_str( $_POST['data'], $form );
120
+ // }
121
+ //
122
+ // $text = '';
123
+ // if( isset( $form['wpstg_disable_text'] ) ) {
124
+ // $text = implode( "\n\r", $form['wpstg_disable_text'] );
125
+ // }
126
+ //
127
+ // $headers = array();
128
+ //
129
+ // $from = isset( $form['wpstg_disable_from'] ) ? $form['wpstg_disable_from'] : '';
130
+ // if( $from ) {
131
+ // $headers[] = "From: $from";
132
+ // $headers[] = "Reply-To: $from";
133
+ // }
134
+ //
135
+ // $subject = isset( $form['wpstg_disable_reason'] ) ? $form['wpstg_disable_reason'] : '(no reason given)';
136
+ //
137
+ // $success = wp_mail( 'makebetter@mashshare.net', $subject, $text, $headers );
138
+ //
139
+ // if( $success ) {
140
+ // wp_die( 1 );
141
+ // }
142
+ // wp_die( 0 );
143
+ // //error_log(print_r($success, true));
144
+ // //error_log($from . $subject . var_dump($form));
145
+ // die();
146
+ //}
147
+ //
148
+ //add_action( 'wp_ajax_wpstg_send_feedback', 'wpstg_send_feedback' );
149
+
apps/Backend/Modules/Jobs/Cloning.php CHANGED
@@ -242,7 +242,9 @@ class Cloning extends Job {
242
private function getDestinationDir() {
243
// No custom clone dir or clone dir equals abspath of main wordpress site
244
if( empty( $this->options->cloneDir ) || $this->options->cloneDir == ( string ) \WPStaging\WPStaging::getWPpath() ) {
245
- return trailingslashit( \WPStaging\WPStaging::getWPpath() . $this->options->cloneDirectoryName );
246
}
247
return trailingslashit( $this->options->cloneDir );
248
}
242
private function getDestinationDir() {
243
// No custom clone dir or clone dir equals abspath of main wordpress site
244
if( empty( $this->options->cloneDir ) || $this->options->cloneDir == ( string ) \WPStaging\WPStaging::getWPpath() ) {
245
+ $this->options->cloneDir = trailingslashit( \WPStaging\WPStaging::getWPpath() . $this->options->cloneDirectoryName );
246
+ //return trailingslashit( \WPStaging\WPStaging::getWPpath() . $this->options->cloneDirectoryName );
247
+ return $this->options->cloneDir;
248
}
249
return trailingslashit( $this->options->cloneDir );
250
}
apps/Backend/Modules/Jobs/Data.php CHANGED
@@ -902,7 +902,7 @@ define( 'DB_COLLATE', '" . DB_COLLATE . "' );\r\n";
902
903
$error = isset( $this->db->last_error ) ? 'Last error: ' . $this->db->last_error : '';
904
905
- $this->log( "Updating upload_path in {$this->prefix}options. {$error}" );
906
907
$updateOptions = $this->db->query(
908
$this->db->prepare(
902
903
$error = isset( $this->db->last_error ) ? 'Last error: ' . $this->db->last_error : '';
904
905
+ $this->log( "Preparing Data Step13: Updating upload_path in {$this->prefix}options. {$error}" );
906
907
$updateOptions = $this->db->query(
908
$this->db->prepare(
apps/Backend/Modules/Jobs/Database.php CHANGED
@@ -54,7 +54,8 @@ class Database extends JobExecutable {
54
* @return boolean
55
*/
56
private function isFatalError() {
57
- $path = trailingslashit( get_home_path() ) . $this->options->cloneDirectoryName;
58
if( isset( $this->options->mainJob ) && $this->options->mainJob !== 'updating' && is_dir( $path ) ) {
59
$this->returnException( " Can not continue! Change the name of the clone or delete existing folder. Then try again. Folder already exists: " . $path );
60
}
@@ -121,7 +122,7 @@ class Database extends JobExecutable {
121
// Make sure prefix of staging site is NEVER identical to prefix of live site!
122
if( $stagingPrefix == $this->db->prefix ) {
123
$error = 'Fatal error 7: The new database table prefix ' . $stagingPrefix . ' would be identical to the table prefix of the live site. Please open a support ticket at support@wp-staging.com';
124
- $this->returnException($error);
125
wp_die( $error );
126
}
127
return $stagingPrefix;
@@ -249,7 +250,10 @@ class Database extends JobExecutable {
249
}
250
251
$this->log( "DB Copy: {$new} already exists, dropping it first" );
252
$this->db->query( "DROP TABLE {$new}" );
253
}
254
255
/**
54
* @return boolean
55
*/
56
private function isFatalError() {
57
+ //$path = trailingslashit( get_home_path() ) . $this->options->cloneDirectoryName;
58
+ $path = trailingslashit($this->options->cloneDir);
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
}
122
// Make sure prefix of staging site is NEVER identical to prefix of live site!
123
if( $stagingPrefix == $this->db->prefix ) {
124
$error = 'Fatal error 7: The new database table prefix ' . $stagingPrefix . ' would be identical to the table prefix of the live site. Please open a support ticket at support@wp-staging.com';
125
+ $this->returnException( $error );
126
wp_die( $error );
127
}
128
return $stagingPrefix;
250
}
251
252
$this->log( "DB Copy: {$new} already exists, dropping it first" );
253
+
254
+ $this->db->query( "SET FOREIGN_KEY_CHECKS=0" );
255
$this->db->query( "DROP TABLE {$new}" );
256
+ $this->db->query( "SET FOREIGN_KEY_CHECKS=1" );
257
}
258
259
/**
apps/Backend/Modules/Jobs/Multisite/Database.php CHANGED
@@ -1,363 +1,363 @@
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( !isset( $this->options->isRunning ) || $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
- $error = 'Fatal error 7: The new database table prefix ' . $stagingPrefix . ' would be identical to the table prefix of the live site. Please open a support ticket at support@wp-staging.com';
129
- $this->returnException($error);
130
- wp_die( $error );
131
-
132
- }
133
- return $stagingPrefix;
134
- }
135
-
136
- /**
137
- * No worries, SQL queries don't eat from PHP execution time!
138
- * @param string $tableName
139
- * @return bool
140
- */
141
- private function copyTable( $tableName ) {
142
-
143
- $strings = new Strings();
144
- $tableName = is_object( $tableName ) ? $tableName->name : $tableName;
145
- $newTableName = $this->getStagingPrefix() . $strings->str_replace_first( $this->db->prefix, null, $tableName );
146
-
147
- // Get wp_users from main site
148
- if( 'users' === $strings->str_replace_first( $this->db->prefix, null, $tableName ) ) {
149
- $tableName = $this->db->base_prefix . 'users';
150
- }
151
- // Get wp_usermeta from main site
152
- if( 'usermeta' === $strings->str_replace_first( $this->db->prefix, null, $tableName ) ) {
153
- $tableName = $this->db->base_prefix . 'usermeta';
154
- }
155
-
156
- // Drop table if necessary
157
- $this->dropTable( $newTableName );
158
-
159
- // Save current job
160
- $this->setJob( $newTableName );
161
-
162
- // Beginning of the job
163
- if( !$this->startJob( $newTableName, $tableName ) ) {
164
- return true;
165
- }
166
-
167
- // Copy data
168
- $this->copyData( $newTableName, $tableName );
169
-
170
- // Finish the step
171
- return $this->finishStep();
172
- }
173
-
174
- /**
175
- * Copy multisite global user table wp_users to wpstgX_users
176
- * @return bool
177
- */
178
- // private function copyWpUsers() {
179
- //// $strings = new Strings();
180
- //// $tableName = $this->db->base_prefix . 'users';
181
- //// $newTableName = $this->getStagingPrefix() . $strings->str_replace_first( $this->db->base_prefix, null, $tableName );
182
- //
183
- // $tableName = $this->db->base_prefix . 'users';
184
- // $newTableName = $this->getStagingPrefix() . 'users';
185
- //
186
- // $this->log( "DB Copy: Try to create table {$newTableName}" );
187
- //
188
- // // Drop table if necessary
189
- // $this->dropTable( $newTableName );
190
- //
191
- // // Save current job
192
- // $this->setJob( $newTableName );
193
- //
194
- // // Beginning of the job
195
- // if( !$this->startJob( $newTableName, $tableName ) ) {
196
- // return true;
197
- // }
198
- //
199
- // // Copy data
200
- // $this->copyData( $newTableName, $tableName );
201
- //
202
- // // Finish the step
203
- // return $this->finishStep();
204
- // }
205
-
206
- /**
207
- * Copy multisite global user table wp_usermeta to wpstgX_users
208
- * @return bool
209
- */
210
- // private function copyWpUsermeta() {
211
- //// $strings = new Strings();
212
- //// $tableName = $this->db->base_prefix . 'usermeta';
213
- //// $newTableName = $this->getStagingPrefix() . $strings->str_replace_first( $this->db->base_prefix, null, $tableName );
214
- // $tableName = $this->db->base_prefix . 'usermeta';
215
- // $newTableName = $this->getStagingPrefix() . 'usermeta';
216
- //
217
- // $this->log( "DB Copy: Try to create table {$newTableName}" );
218
- //
219
- //
220
- // // Drop table if necessary
221
- // $this->dropTable( $newTableName );
222
- //
223
- // // Save current job
224
- // $this->setJob( $newTableName );
225
- //
226
- // // Beginning of the job
227
- // if( !$this->startJob( $newTableName, $tableName ) ) {
228
- // return true;
229
- // }
230
- // // Copy data
231
- // $this->copyData( $newTableName, $tableName );
232
- //
233
- // // Finish the step
234
- // return $this->finishStep();
235
- // }
236
-
237
- /**
238
- * Copy data from old table to new table
239
- * @param string $new
240
- * @param string $old
241
- */
242
- private function copyData( $new, $old ) {
243
- $rows = $this->options->job->start + $this->settings->queryLimit;
244
- $this->log(
245
- "DB Copy: {$old} as {$new} from {$this->options->job->start} to {$rows} records"
246
- );
247
-
248
- $limitation = '';
249
-
250
- if( 0 < ( int ) $this->settings->queryLimit ) {
251
- $limitation = " LIMIT {$this->settings->queryLimit} OFFSET {$this->options->job->start}";
252
- }
253
-
254
- $this->db->query(
255
- "INSERT INTO {$new} SELECT * FROM {$old} {$limitation}"
256
- );
257
-
258
- // Set new offset
259
- $this->options->job->start += $this->settings->queryLimit;
260
- }
261
-
262
- /**
263
- * Set the job
264
- * @param string $table
265
- */
266
- private function setJob( $table ) {
267
- if( isset( $this->options->job->current ) ) {
268
- return;
269
- }
270
-
271
- $this->options->job->current = $table;
272
- $this->options->job->start = 0;
273
- }
274
-
275
- /**
276
- * Start Job
277
- * @param string $new
278
- * @param string $old
279
- * @return bool
280
- */
281
- private function startJob( $new, $old ) {
282
-
283
- if( 0 != $this->options->job->start ) {
284
- return true;
285
- }
286
-
287
- // Table does not exist
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 = 0;
298
- $this->options->job->total = ( int ) $this->db->get_var( "SELECT COUNT(1) FROM {$old}" );
299
-
300
- if( 0 == $this->options->job->total ) {
301
- $this->finishStep();
302
- return false;
303
- }
304
-
305
- return true;
306
- }
307
-
308
- /**
309
- * Finish the step
310
- */
311
- private function finishStep() {
312
- // This job is not finished yet
313
- if( $this->options->job->total > $this->options->job->start ) {
314
- return false;
315
- }
316
-
317
- // Add it to cloned tables listing
318
- $this->options->clonedTables[] = isset( $this->options->tables[$this->options->currentStep] ) ? $this->options->tables[$this->options->currentStep] : false;
319
-
320
- // Reset job
321
- $this->options->job = new \stdClass();
322
-
323
- return true;
324
- }
325
-
326
- /**
327
- * Drop table if necessary
328
- * @param string $new
329
- */
330
- private function dropTable( $new ) {
331
-
332
- $old = $this->db->get_var( $this->db->prepare( "SHOW TABLES LIKE %s", $new ) );
333
-
334
- if( !$this->shouldDropTable( $new, $old ) ) {
335
- return;
336
- }
337
-
338
- $this->log( "DB Copy: {$new} already exists, dropping it first" );
339
- $this->db->query( "DROP TABLE {$new}" );
340
- }
341
-
342
- /**
343
- * Check if table needs to be dropped
344
- * @param string $new
345
- * @param string $old
346
- * @return bool
347
- */
348
- private function shouldDropTable( $new, $old ) {
349
-
350
-
351
-
352
- if( $old === $new &&
353
- (
354
- !isset( $this->options->job->current ) ||
355
- !isset( $this->options->job->start ) ||
356
- 0 == $this->options->job->start
357
- ) ) {
358
- return true;
359
- }
360
- return false;
361
- }
362
-
363
- }
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( !isset( $this->options->isRunning ) || $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
+ $error = 'Fatal error 7: The new database table prefix ' . $stagingPrefix . ' would be identical to the table prefix of the live site. Please open a support ticket at support@wp-staging.com';
129
+ $this->returnException($error);
130
+ wp_die( $error );
131
+
132
+ }
133
+ return $stagingPrefix;
134
+ }
135
+
136
+ /**
137
+ * No worries, SQL queries don't eat from PHP execution time!
138
+ * @param string $tableName
139
+ * @return bool
140
+ */
141
+ private function copyTable( $tableName ) {
142
+
143
+ $strings = new Strings();
144
+ $tableName = is_object( $tableName ) ? $tableName->name : $tableName;
145
+ $newTableName = $this->getStagingPrefix() . $strings->str_replace_first( $this->db->prefix, null, $tableName );
146
+
147
+ // Get wp_users from main site
148
+ if( 'users' === $strings->str_replace_first( $this->db->prefix, null, $tableName ) ) {
149
+ $tableName = $this->db->base_prefix . 'users';
150
+ }
151
+ // Get wp_usermeta from main site
152
+ if( 'usermeta' === $strings->str_replace_first( $this->db->prefix, null, $tableName ) ) {
153
+ $tableName = $this->db->base_prefix . 'usermeta';
154
+ }
155
+
156
+ // Drop table if necessary
157
+ $this->dropTable( $newTableName );
158
+
159
+ // Save current job
160
+ $this->setJob( $newTableName );
161
+
162
+ // Beginning of the job
163
+ if( !$this->startJob( $newTableName, $tableName ) ) {
164
+ return true;
165
+ }
166
+
167
+ // Copy data
168
+ $this->copyData( $newTableName, $tableName );
169
+
170
+ // Finish the step
171
+ return $this->finishStep();
172
+ }
173
+
174
+ /**
175
+ * Copy multisite global user table wp_users to wpstgX_users
176
+ * @return bool
177
+ */
178
+ // private function copyWpUsers() {
179
+ //// $strings = new Strings();
180
+ //// $tableName = $this->db->base_prefix . 'users';
181
+ //// $newTableName = $this->getStagingPrefix() . $strings->str_replace_first( $this->db->base_prefix, null, $tableName );
182
+ //
183
+ // $tableName = $this->db->base_prefix . 'users';
184
+ // $newTableName = $this->getStagingPrefix() . 'users';
185
+ //
186
+ // $this->log( "DB Copy: Try to create table {$newTableName}" );
187
+ //
188
+ // // Drop table if necessary
189
+ // $this->dropTable( $newTableName );
190
+ //
191
+ // // Save current job
192
+ // $this->setJob( $newTableName );
193
+ //
194
+ // // Beginning of the job
195
+ // if( !$this->startJob( $newTableName, $tableName ) ) {
196
+ // return true;
197
+ // }
198
+ //
199
+ // // Copy data
200
+ // $this->copyData( $newTableName, $tableName );
201
+ //
202
+ // // Finish the step
203
+ // return $this->finishStep();
204
+ // }
205
+
206
+ /**
207
+ * Copy multisite global user table wp_usermeta to wpstgX_users
208
+ * @return bool
209
+ */
210
+ // private function copyWpUsermeta() {
211
+ //// $strings = new Strings();
212
+ //// $tableName = $this->db->base_prefix . 'usermeta';
213
+ //// $newTableName = $this->getStagingPrefix() . $strings->str_replace_first( $this->db->base_prefix, null, $tableName );
214
+ // $tableName = $this->db->base_prefix . 'usermeta';
215
+ // $newTableName = $this->getStagingPrefix() . 'usermeta';
216
+ //
217
+ // $this->log( "DB Copy: Try to create table {$newTableName}" );
218
+ //
219
+ //
220
+ // // Drop table if necessary
221
+ // $this->dropTable( $newTableName );
222
+ //
223
+ // // Save current job
224
+ // $this->setJob( $newTableName );
225
+ //
226
+ // // Beginning of the job
227
+ // if( !$this->startJob( $newTableName, $tableName ) ) {
228
+ // return true;
229
+ // }
230
+ // // Copy data
231
+ // $this->copyData( $newTableName, $tableName );
232
+ //
233
+ // // Finish the step
234
+ // return $this->finishStep();
235
+ // }
236
+
237
+ /**
238
+ * Copy data from old table to new table
239
+ * @param string $new
240
+ * @param string $old
241
+ */
242
+ private function copyData( $new, $old ) {
243
+ $rows = $this->options->job->start + $this->settings->queryLimit;
244
+ $this->log(
245
+ "DB Copy: {$old} as {$new} from {$this->options->job->start} to {$rows} records"
246
+ );
247
+
248
+ $limitation = '';
249
+
250
+ if( 0 < ( int ) $this->settings->queryLimit ) {
251
+ $limitation = " LIMIT {$this->settings->queryLimit} OFFSET {$this->options->job->start}";
252
+ }
253
+
254
+ $this->db->query(
255
+ "INSERT INTO {$new} SELECT * FROM {$old} {$limitation}"
256
+ );
257
+
258
+ // Set new offset
259
+ $this->options->job->start += $this->settings->queryLimit;
260
+ }
261
+
262
+ /**
263
+ * Set the job
264
+ * @param string $table
265
+ */
266
+ private function setJob( $table ) {
267
+ if( isset( $this->options->job->current ) ) {
268
+ return;
269
+ }
270
+
271
+ $this->options->job->current = $table;
272
+ $this->options->job->start = 0;
273
+ }
274
+
275
+ /**
276
+ * Start Job
277
+ * @param string $new
278
+ * @param string $old
279
+ * @return bool
280
+ */
281
+ private function startJob( $new, $old ) {
282
+
283
+ if( 0 != $this->options->job->start ) {
284
+ return true;
285
+ }
286
+
287
+ // Table does not exist
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 = 0;
298
+ $this->options->job->total = ( int ) $this->db->get_var( "SELECT COUNT(1) FROM {$old}" );
299
+
300
+ if( 0 == $this->options->job->total ) {
301
+ $this->finishStep();
302
+ return false;
303
+ }
304
+
305
+ return true;
306
+ }
307
+
308
+ /**
309
+ * Finish the step
310
+ */
311
+ private function finishStep() {
312
+ // This job is not finished yet
313
+ if( $this->options->job->total > $this->options->job->start ) {
314
+ return false;
315
+ }
316
+
317
+ // Add it to cloned tables listing
318
+ $this->options->clonedTables[] = isset( $this->options->tables[$this->options->currentStep] ) ? $this->options->tables[$this->options->currentStep] : false;
319
+
320
+ // Reset job
321
+ $this->options->job = new \stdClass();
322
+
323
+ return true;
324
+ }
325
+
326
+ /**
327
+ * Drop table if necessary
328
+ * @param string $new
329
+ */
330
+ private function dropTable( $new ) {
331
+
332
+ $old = $this->db->get_var( $this->db->prepare( "SHOW TABLES LIKE %s", $new ) );
333
+
334
+ if( !$this->shouldDropTable( $new, $old ) ) {
335
+ return;
336
+ }
337
+
338
+ $this->log( "DB Copy: {$new} already exists, dropping it first" );
339
+ $this->db->query( "DROP TABLE {$new}" );
340
+ }
341
+
342
+ /**
343
+ * Check if table needs to be dropped
344
+ * @param string $new
345
+ * @param string $old
346
+ * @return bool
347
+ */
348
+ private function shouldDropTable( $new, $old ) {
349
+
350
+
351
+
352
+ if( $old === $new &&
353
+ (
354
+ !isset( $this->options->job->current ) ||
355
+ !isset( $this->options->job->start ) ||
356
+ 0 == $this->options->job->start
357
+ ) ) {
358
+ return true;
359
+ }
360
+ return false;
361
+ }
362
+
363
+ }
apps/Backend/Modules/Jobs/Multisite/SearchReplace.php CHANGED
@@ -1,816 +1,816 @@
1
- <?php
2
-
3
- namespace WPStaging\Backend\Modules\Jobs\Multisite;
4
-
5
- // No Direct Access
6
- if( !defined( "WPINC" ) ) {
7
- die;
8
- }
9
-
10
- use WPStaging\WPStaging;
11
- use WPStaging\Utils\Strings;
12
- use WPStaging\Utils\Helper;
13
- use WPStaging\Utils\Multisite;
14
- use WPStaging\Backend\Modules\Jobs\JobExecutable;
15
-
16
- /**
17
- * Class Database
18
- * @package WPStaging\Backend\Modules\Jobs
19
- */
20
- class SearchReplace extends JobExecutable {
21
-
22
- /**
23
- * @var int
24
- */
25
- private $total = 0;
26
-
27
- /**
28
- * @var \WPDB
29
- */
30
- public $db;
31
-
32
- /**
33
- *
34
- * @var Obj
35
- */
36
- private $strings;
37
-
38
- /**
39
- *
40
- * @var string
41
- */
42
- private $destinationHostname;
43
-
44
- /**
45
- *
46
- * @var string
47
- */
48
- private $sourceHostname;
49
-
50
- /**
51
- *
52
- * @var string
53
- */
54
- //private $targetDir;
55
-
56
- /**
57
- * The prefix of the new database tables which are used for the live site after updating tables
58
- * @var string
59
- */
60
- public $tmpPrefix;
61
-
62
- /**
63
- * Initialize
64
- */
65
- public function initialize() {
66
- $this->total = count( $this->options->tables );
67
- $this->db = WPStaging::getInstance()->get( "wpdb" );
68
- $this->tmpPrefix = $this->options->prefix;
69
- $this->strings = new Strings();
70
- $this->sourceHostname = $this->getSourceHostname();
71
- $this->destinationHostname = $this->getDestinationHostname();
72
- }
73
-
74
- public function start() {
75
- // Skip job. Nothing to do
76
- if( $this->options->totalSteps === 0 ) {
77
- $this->prepareResponse( true, false );
78
- }
79
-
80
- $this->run();
81
-
82
- // Save option, progress
83
- $this->saveOptions();
84
-
85
- return ( object ) $this->response;
86
- }
87
-
88
- /**
89
- * Calculate Total Steps in This Job and Assign It to $this->options->totalSteps
90
- * @return void
91
- */
92
- protected function calculateTotalSteps() {
93
- $this->options->totalSteps = $this->total;
94
- }
95
-
96
- /**
97
- * Execute the Current Step
98
- * Returns false when over threshold limits are hit or when the job is done, true otherwise
99
- * @return bool
100
- */
101
- protected function execute() {
102
- // Over limits threshold
103
- if( $this->isOverThreshold() ) {
104
- // Prepare response and save current progress
105
- $this->prepareResponse( false, false );
106
- $this->saveOptions();
107
- return false;
108
- }
109
-
110
- // No more steps, finished
111
- if( $this->options->currentStep > $this->total || !isset( $this->options->tables[$this->options->currentStep] ) ) {
112
- $this->prepareResponse( true, false );
113
- return false;
114
- }
115
-
116
- // Table is excluded
117
- if( in_array( $this->options->tables[$this->options->currentStep], $this->options->excludedTables ) ) {
118
- $this->prepareResponse();
119
- return true;
120
- }
121
-
122
- // Search & Replace
123
- if( !$this->stopExecution() && !$this->updateTable( $this->options->tables[$this->options->currentStep] ) ) {
124
- // Prepare Response
125
- $this->prepareResponse( false, false );
126
-
127
- // Not finished
128
- return true;
129
- }
130
-
131
-
132
- // Prepare Response
133
- $this->prepareResponse();
134
-
135
- // Not finished
136
- return true;
137
- }
138
-
139
- /**
140
- * Stop Execution immediately
141
- * return mixed bool | json
142
- */
143
- private function stopExecution() {
144
- if( $this->db->prefix == $this->tmpPrefix ) {
145
- $this->returnException( 'Fatal Error 9: Prefix ' . $this->db->prefix . ' is used for the live site hence it can not be used for the staging site as well. Please ask support@wp-staging.com how to resolve this.' );
146
- }
147
- return false;
148
- }
149
-
150
- /**
151
- * Copy Tables
152
- * @param string $tableName
153
- * @return bool
154
- */
155
- private function updateTable( $tableName ) {
156
- $strings = new Strings();
157
- $table = $strings->str_replace_first( $this->db->prefix, '', $tableName );
158
- $newTableName = $this->tmpPrefix . $table;
159
-
160
- // Save current job
161
- $this->setJob( $newTableName );
162
-
163
- // Beginning of the job
164
- if( !$this->startJob( $newTableName, $tableName ) ) {
165
- return true;
166
- }
167
- // Copy data
168
- $this->startReplace( $newTableName );
169
-
170
- // Finish the step
171
- return $this->finishStep();
172
- }
173
-
174
- /**
175
- * Get source Hostname depending on wheather WP has been installed in sub dir or not
176
- * @return type
177
- */
178
- public function getSourceHostname() {
179
-
180
- if( $this->isSubDir() ) {
181
- return trailingslashit( $this->multisiteHomeUrlWithoutScheme ) . '/' . $this->getSubDir();
182
- }
183
- return $this->multisiteHomeUrlWithoutScheme;
184
- }
185
-
186
- /**
187
- * Get destination Hostname depending on wheather WP has been installed in sub dir or not
188
- * @return type
189
- */
190
- public function getDestinationHostname() {
191
-
192
- if( !empty( $this->options->cloneHostname ) ) {
193
- return $this->strings->getUrlWithoutScheme( $this->options->cloneHostname );
194
- }
195
-
196
- if( $this->isSubDir() ) {
197
- return trailingslashit( $this->strings->getUrlWithoutScheme( $this->multisiteDomainWithoutScheme ) ) . $this->getSubDir() . '/' . $this->options->cloneDirectoryName;
198
- }
199
-
200
- // Get the path to the main multisite without appending and trailingslash e.g. wordpress
201
- $multisitePath = defined( 'PATH_CURRENT_SITE') ? PATH_CURRENT_SITE : '/';
202
- $url = rtrim( $this->strings->getUrlWithoutScheme( $this->multisiteDomainWithoutScheme ), '/\\' ) . $multisitePath . $this->options->cloneDirectoryName;
203
- //$multisitePath = defined( 'PATH_CURRENT_SITE' ) ? str_replace( '/', '', PATH_CURRENT_SITE ) : '';
204
- //$url = trailingslashit( $this->strings->getUrlWithoutScheme( $this->multisiteDomainWithoutScheme ) ) . $multisitePath . '/' . $this->options->cloneDirectoryName;
205
- return $url;
206
- }
207
-
208
- /**
209
- * Get the install sub directory if WP is installed in sub directory
210
- * @return string
211
- */
212
- private function getSubDir() {
213
- $home = get_option( 'home' );
214
- $siteurl = get_option( 'siteurl' );
215
-
216
- if( empty( $home ) || empty( $siteurl ) ) {
217
- return '';
218
- }
219
-
220
- $dir = str_replace( $home, '', $siteurl );
221
- return str_replace( '/', '', $dir );
222
- }
223
-
224
- /**
225
- * Start search replace job
226
- * @param string $new
227
- * @param string $old
228
- */
229
- private function startReplace( $table ) {
230
- $rows = $this->options->job->start + $this->settings->querySRLimit;
231
- $this->log(
232
- "DB Processing: Table {$table} {$this->options->job->start} to {$rows} records"
233
- );
234
-
235
- // Search & Replace
236
- $this->searchReplace( $table, $rows, array() );
237
-
238
- // Set new offset
239
- $this->options->job->start += $this->settings->querySRLimit;
240
- }
241
-
242
- /**
243
- * Returns the number of pages in a table.
244
- * @access public
245
- * @return int
246
- */
247
- private function get_pages_in_table( $table ) {
248
-
249
- // Table does not exist
250
- $result = $this->db->query( "SHOW TABLES LIKE '{$table}'" );
251
- if( !$result || 0 === $result ) {
252
- return 0;
253
- }
254
-
255
- $table = esc_sql( $table );
256
- $rows = $this->db->get_var( "SELECT COUNT(*) FROM $table" );
257
- $pages = ceil( $rows / $this->settings->querySRLimit );
258
- return absint( $pages );
259
- }
260
-
261
- /**
262
- * Gets the columns in a table.
263
- * @access public
264
- * @param string $table The table to check.
265
- * @return array
266
- */
267
- private function get_columns( $table ) {
268
- $primary_key = null;
269
- $columns = array();
270
- $fields = $this->db->get_results( 'DESCRIBE ' . $table );
271
- if( is_array( $fields ) ) {
272
- foreach ( $fields as $column ) {
273
- $columns[] = $column->Field;
274
- if( $column->Key == 'PRI' ) {
275
- $primary_key = $column->Field;
276
- }
277
- }
278
- }
279
- return array($primary_key, $columns);
280
- }
281
-
282
- /**
283
- * Return absolute destination path
284
- * @return string
285
- */
286
- private function getAbsDestination() {
287
- if( empty( $this->options->cloneDir ) ) {
288
- return \WPStaging\WPStaging::getWPpath();
289
- }
290
- return trailingslashit( $this->options->cloneDir );
291
- }
292
-
293
- /**
294
- * Adapated from interconnect/it's search/replace script, adapted from Better Search Replace
295
- *
296
- * Modified to use WordPress wpdb functions instead of PHP's native mysql/pdo functions,
297
- * and to be compatible with batch processing.
298
- *
299
- * @link https://interconnectit.com/products/search-and-replace-for-wordpress-databases/
300
- *
301
- * @access public
302
- * @param string $table The table to run the replacement on.
303
- * @param int $page The page/block to begin the query on.
304
- * @param array $args An associative array containing arguments for this run.
305
- * @return array
306
- */
307
- private function searchReplace( $table, $page, $args ) {
308
-
309
- if( $this->thirdParty->isSearchReplaceExcluded( $table ) ) {
310
- $this->log( "DB Processing: Skip {$table}", \WPStaging\Utils\Logger::TYPE_INFO );
311
- return true;
312
- }
313
-
314
- // Load up the default settings for this chunk.
315
- $table = esc_sql( $table );
316
- $current_page = $this->options->job->start + $this->settings->querySRLimit;
317
- $pages = $this->get_pages_in_table( $table );
318
-
319
-
320
- // Search URL example.com/staging and root path to staging site /var/www/htdocs/staging
321
- $args['search_for'] = array(
322
- '//' . $this->getSourceHostname(),
323
- ABSPATH,
324
- '\/\/' . str_replace( '/', '\/', $this->getSourceHostname() ), // Used by revslider and several visual editors
325
- '%2F%2F' . str_replace( '/', '%2F', $this->getSourceHostname() ), // HTML entitity for WP Backery Page Builder Plugin
326
- //$this->getImagePathLive()
327
- );
328
-
329
-
330
- $args['replace_with'] = array(
331
- '//' . $this->getDestinationHostname(),
332
- $this->options->destinationDir,
333
- '\/\/' . str_replace( '/', '\/', $this->getDestinationHostname() ), // Used by revslider and several visual editors
334
- '%2F%2F' . str_replace( '/', '%2F', $this->getDestinationHostname() ), // HTML entitity for WP Backery Page Builder Plugin
335
- //$this->getImagePathStaging()
336
- );
337
-
338
- $this->debugLog( "DB Processing: Search: {$args['search_for'][0]}", \WPStaging\Utils\Logger::TYPE_INFO );
339
- $this->debugLog( "DB Processing: Replace: {$args['replace_with'][0]}", \WPStaging\Utils\Logger::TYPE_INFO );
340
-
341
-
342
-
343
- $args['replace_guids'] = 'off';
344
- $args['dry_run'] = 'off';
345
- $args['case_insensitive'] = false;
346
- //$args['replace_mails'] = 'off';
347
- $args['skip_transients'] = 'on';
348
-
349
-
350
- // Allow filtering of search & replace parameters
351
- $args = apply_filters( 'wpstg_clone_searchreplace_params', $args );
352
-
353
- // Get a list of columns in this table.
354
- list( $primary_key, $columns ) = $this->get_columns( $table );
355
-
356
- // Bail out early if there isn't a primary key.
357
- // We commented this to search & replace through tables which have no primary keys like wp_revslider_slides
358
- // @todo test this carefully. If it causes (performance) issues we need to activate it again!
359
- // @since 2.4.4
360
- // if( null === $primary_key ) {
361
- // return false;
362
- // }
363
-
364
- $current_row = 0;
365
- $start = $this->options->job->start;
366
- $end = $this->settings->querySRLimit;
367
-
368
- // Grab the content of the table.
369
- $data = $this->db->get_results( "SELECT * FROM $table LIMIT $start, $end", ARRAY_A );
370
-
371
- // Filter certain rows option_name in wpstg_options
372
- $filter = array(
373
- 'Admin_custome_login_Slidshow',
374
- 'Admin_custome_login_Social',
375
- 'Admin_custome_login_logo',
376
- 'Admin_custome_login_text',
377
- 'Admin_custome_login_login',
378
- 'Admin_custome_login_top',
379
- 'Admin_custome_login_dashboard',
380
- 'Admin_custome_login_Version',
381
- 'upload_path',
382
- );
383
-
384
- $filter = apply_filters( 'wpstg_clone_searchreplace_excl_rows', $filter );
385
-
386
- // Loop through the data.
387
- foreach ( $data as $row ) {
388
- $current_row++;
389
- $update_sql = array();
390
- $where_sql = array();
391
- $upd = false;
392
-
393
- // Skip rows below
394
- if( isset( $row['option_name'] ) && in_array( $row['option_name'], $filter ) ) {
395
- continue;
396
- }
397
-
398
- // Skip rows with transients (They can store huge data and we need to save memory)
399
- if( isset( $row['option_name'] ) && 'on' === $args['skip_transients'] && false !== strpos( $row['option_name'], '_transient' ) ) {
400
- continue;
401
- }
402
- // Skip rows with more than 5MB to save memory
403
- if( isset( $row['option_value'] ) && strlen( $row['option_value'] ) >= 5000000 ) {
404
- continue;
405
- }
406
-
407
-
408
- foreach ( $columns as $column ) {
409
-
410
- $dataRow = $row[$column];
411
-
412
- // Skip rows larger than 5MB
413
- $size = strlen( $dataRow );
414
- if( $size >= 5000000 ) {
415
- continue;
416
- }
417
-
418
- // Skip Primary key
419
- if( $column == $primary_key ) {
420
- $where_sql[] = $column . ' = "' . $this->mysql_escape_mimic( $dataRow ) . '"';
421
- continue;
422
- }
423
-
424
- // Skip GUIDs by default.
425
- if( 'on' !== $args['replace_guids'] && 'guid' == $column ) {
426
- continue;
427
- }
428
-
429
- // Skip mail addresses
430
- // if( 'off' === $args['replace_mails'] && false !== strpos( $dataRow, '@' . $this->multisiteDomainWithoutScheme ) ) {
431
- // continue;
432
- // }
433
- // Check options table
434
- if( $this->options->prefix . 'options' === $table ) {
435
-
436
- // Skip certain options
437
- // if( isset( $should_skip ) && true === $should_skip ) {
438
- // $should_skip = false;
439
- // continue;
440
- // }
441
- // Skip this row
442
- if( 'wpstg_existing_clones_beta' === $dataRow ||
443
- 'wpstg_existing_clones' === $dataRow ||
444
- 'wpstg_settings' === $dataRow ||
445
- 'wpstg_license_status' === $dataRow ||
446
- 'siteurl' === $dataRow ||
447
- 'home' === $dataRow
448
- ) {
449
- //$should_skip = true;
450
- continue;
451
- }
452
- }
453
-
454
- // Check the path delimiter for / or \/ and remove one of those which prevents from resulting in wrong syntax like domain.com/staging\/.
455
- // 1. local.wordpress.test -> local.wordpress.test/staging
456
- // 2. local.wordpress.test\/ -> local.wordpress.test\/staging\/
457
- $tmp = $args;
458
- if( false === strpos( $dataRow, $tmp['search_for'][0] ) ) {
459
- array_shift( $tmp['search_for'] ); // rtrim( $this->homeUrl, '/' ),
460
- array_shift( $tmp['replace_with'] ); // rtrim( $this->homeUrl, '/' ) . '/' . $this->options->cloneDirectoryName,
461
- } else {
462
- unset( $tmp['search_for'][1] );
463
- unset( $tmp['replace_with'][1] );
464
- // recount array
465
- $tmp['search_for'] = array_values( $tmp['search_for'] );
466
- $tmp['replace_with'] = array_values( $tmp['replace_with'] );
467
- }
468
-
469
- // Run a search replace on the data row and respect the serialisation.
470
- $i = 0;
471
- foreach ( $tmp['search_for'] as $replace ) {
472
- $dataRow = $this->recursive_unserialize_replace( $tmp['search_for'][$i], $tmp['replace_with'][$i], $dataRow, false, $args['case_insensitive'] );
473
- $i++;
474
- }
475
- unset( $replace );
476
- unset( $i );
477
- unset( $tmp );
478
-
479
- // Something was changed
480
- if( $row[$column] != $dataRow ) {
481
- $update_sql[] = $column . ' = "' . $this->mysql_escape_mimic( $dataRow ) . '"';
482
- $upd = true;
483
- }
484
- }
485
-
486
- // Determine what to do with updates.
487
- if( $args['dry_run'] === 'on' ) {
488
- // Don't do anything if a dry run
489
- } elseif( $upd && !empty( $where_sql ) ) {
490
- // If there are changes to make, run the query.
491
- $sql = 'UPDATE ' . $table . ' SET ' . implode( ', ', $update_sql ) . ' WHERE ' . implode( ' AND ', array_filter( $where_sql ) );
492
- $result = $this->db->query( $sql );
493
-
494
- if( !$result ) {
495
- $this->log( "Error updating row {$current_row} SQL: {$sql}", \WPStaging\Utils\Logger::TYPE_ERROR );
496
- }
497
- }
498
- } // end row loop
499
- unset( $row );
500
- unset( $update_sql );
501
- unset( $where_sql );
502
- unset( $sql );
503
-
504
-
505
- // DB Flush
506
- $this->db->flush();
507
- return true;
508
- }
509
-
510
- /**
511
- * Get path to multisite image folder e.g. wp-content/blogs.dir/ID/files or wp-content/uploads/sites/ID
512
- * @return string
513
- */
514
- private function getImagePathLive() {
515
- // Check first which structure is used
516
- $uploads = wp_upload_dir();
517
- $basedir = $uploads['basedir'];
518
- $blogId = get_current_blog_id();
519
-
520
- if( false === strpos( $basedir, 'blogs.dir' ) ) {
521
- // Since WP 3.5
522
- $path = $blogId > 1 ?
523
- 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'sites' . DIRECTORY_SEPARATOR . get_current_blog_id() . DIRECTORY_SEPARATOR :
524
- 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR;
525
- } else {
526
- // old blog structure
527
- $path = $blogId > 1 ?
528
- 'wp-content' . DIRECTORY_SEPARATOR . 'blogs.dir' . DIRECTORY_SEPARATOR . get_current_blog_id() . DIRECTORY_SEPARATOR . 'files' . DIRECTORY_SEPARATOR :
529
- 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR;
530
- }
531
- return $path;
532
- }
533
-
534
- /**
535
- * Get path to staging site image path wp-content/uploads
536
- * @return string
537
- */
538
- private function getImagePathStaging() {
539
- return 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR;
540
- }
541
-
542
- /**
543
- * Adapted from interconnect/it's search/replace script.
544
- *
545
- * @link https://interconnectit.com/products/search-and-replace-for-wordpress-databases/
546
- *
547
- * Take a serialised array and unserialise it replacing elements as needed and
548
- * unserialising any subordinate arrays and performing the replace on those too.
549
- *
550
- * @access private
551
- * @param string $from String we're looking to replace.
552
- * @param string $to What we want it to be replaced with
553
- * @param array $data Used to pass any subordinate arrays back to in.
554
- * @param boolean $serialized Does the array passed via $data need serialising.
555
- * @param sting|boolean $case_insensitive Set to 'on' if we should ignore case, false otherwise.
556
- *
557
- * @return string|array The original array with all elements replaced as needed.
558
- */
559
- private function recursive_unserialize_replace( $from = '', $to = '', $data = '', $serialized = false, $case_insensitive = false ) {
560
- try {
561
- // Some unserialized data cannot be re-serialized eg. SimpleXMLElements
562
- if( is_serialized( $data ) && ( $unserialized = @unserialize( $data ) ) !== false ) {
563
- $data = $this->recursive_unserialize_replace( $from, $to, $unserialized, true, $case_insensitive );
564
- } elseif( is_array( $data ) ) {
565
- $tmp = array();
566
- foreach ( $data as $key => $value ) {
567
- $tmp[$key] = $this->recursive_unserialize_replace( $from, $to, $value, false, $case_insensitive );
568
- }
569
-
570
- $data = $tmp;
571
- unset( $tmp );
572
- } elseif( is_object( $data ) ) {
573
- $tmp = $data;
574
- $props = get_object_vars( $data );
575
-
576
- // Do not continue if class contains __PHP_Incomplete_Class_Name
577
- if( !empty( $props['__PHP_Incomplete_Class_Name'] ) ) {
578
- return $data;
579
-
580
- }
581
-
582
- // Do a search & replace
583
- foreach ( $props as $key => $value ) {
584
- if( $key === '' || ord( $key[0] ) === 0 ) {
585
- continue;
586
- }
587
- $tmp->$key = $this->recursive_unserialize_replace( $from, $to, $value, false, $case_insensitive );
588
- }
589
-
590
- $data = $tmp;
591
- unset( $tmp );
592
- unset( $props );
593
- } else {
594
- if( is_string( $data ) ) {
595
- if( !empty( $from ) && !empty( $to ) ) {
596
- $data = $this->str_replace( $from, $to, $data, $case_insensitive );
597
- }
598
- }
599
- }
600
-
601
- if( $serialized ) {
602
- return serialize( $data );
603
- }
604
- } catch ( Exception $error ) {
605
-
606
- }
607
-
608
- return $data;
609
- }
610
-
611
- /**
612
- * Check if the object is a valid one and not __PHP_Incomplete_Class_Name
613
- * Can not use is_object alone because in php 7.2 it's returning true even though object is __PHP_Incomplete_Class_Name
614
- * @return boolean
615
- */
616
- // private function isValidObject( $data ) {
617
- // if( !is_object( $data ) || gettype( $data ) != 'object' ) {
618
- // return false;
619
- // }
620
- //
621
- // $invalid_class_props = get_object_vars( $data );
622
- //
623
- // if (!isset($invalid_class_props['__PHP_Incomplete_Class_Name'])){
624
- // // Assume it must be an valid object
625
- // return true;
626
- // }
627
- //
628
- // $invalid_object_class = $invalid_class_props['__PHP_Incomplete_Class_Name'];
629
- //
630
- // if( !empty( $invalid_object_class ) ) {
631
- // return false;
632
- // }
633
- //
634
- // // Assume it must be an valid object
635
- // return true;
636
- // }
637
-
638
- /**
639
- * Mimics the mysql_real_escape_string function. Adapted from a post by 'feedr' on php.net.
640
- * @link http://php.net/manual/en/function.mysql-real-escape-string.php#101248
641
- * @access public
642
- * @param string $input The string to escape.
643
- * @return string
644
- */
645
- private function mysql_escape_mimic( $input ) {
646
- if( is_array( $input ) ) {
647
- return array_map( __METHOD__, $input );
648
- }
649
- if( !empty( $input ) && is_string( $input ) ) {
650
- return str_replace( array('\\', "\0", "\n", "\r", "'", '"', "\x1a"), array('\\\\', '\\0', '\\n', '\\r', "\\'", '\\"', '\\Z'), $input );
651
- }
652
-
653
- return $input;
654
- }
655
-
656
- /**
657
- * Return unserialized object or array
658
- *
659
- * @param string $serialized_string Serialized string.
660
- * @param string $method The name of the caller method.
661
- *
662
- * @return mixed, false on failure
663
- */
664
- private static function unserialize( $serialized_string ) {
665
- if( !is_serialized( $serialized_string ) ) {
666
- return false;
667
- }
668
-
669
- $serialized_string = trim( $serialized_string );
670
- $unserialized_string = @unserialize( $serialized_string );
671
-
672
- return $unserialized_string;
673
- }
674
-
675
- /**
676
- * Wrapper for str_replace
677
- *
678
- * @param string $from
679
- * @param string $to
680
- * @param string $data
681
- * @param string|bool $case_insensitive
682
- *
683
- * @return string
684
- */
685
- private function str_replace( $from, $to, $data, $case_insensitive = false ) {
686
-
687
- // Add filter
688
- $excludes = apply_filters( 'wpstg_clone_searchreplace_excl', array() );
689
-
690
- // Build pattern
691
- $regexExclude = '';
692
- foreach ( $excludes as $exclude ) {
693
- $regexExclude .= $exclude . '(*SKIP)(FAIL)|';
694
- }
695
-
696
- if( 'on' === $case_insensitive ) {
697
- //$data = str_ireplace( $from, $to, $data );
698
- $data = preg_replace( '#' . $regexExclude . preg_quote( $from ) . '#i', $to, $data );
699
- } else {
700
- //$data = str_replace( $from, $to, $data );
701
- $data = preg_replace( '#' . $regexExclude . preg_quote( $from ) . '#', $to, $data );
702
- }
703
-
704
- return $data;
705
- }
706
-
707
- /**
708
- * Set the job
709
- * @param string $table
710
- */
711
- private function setJob( $table ) {
712
- if( !empty( $this->options->job->current ) ) {
713
- return;
714
- }
715
-
716
- $this->options->job->current = $table;
717
- $this->options->job->start = 0;
718
- }
719
-
720
- /**
721
- * Start Job
722
- * @param string $new
723
- * @param string $old
724
- * @return bool
725
- */
726
- private function startJob( $new, $old ) {
727
-
728
- $this->options->job->total = 0;
729
-
730
- if( 0 != $this->options->job->start ) {
731
- return true;
732
- }
733
-
734
- // Table does not exist
735
- $result = $this->db->query( "SHOW TABLES LIKE '{$old}'" );
736
- if( !$result || 0 === $result ) {
737
- return false;
738
- }
739
-
740
- $this->options->job->total = ( int ) $this->db->get_var( "SELECT COUNT(1) FROM {$old}" );
741
-
742
- if( 0 == $this->options->job->total ) {
743
- $this->finishStep();
744
- return false;
745
- }
746
-
747
- return true;
748
- }
749
-
750
- /**
751
- * Finish the step
752
- */
753
- private function finishStep() {
754
- // This job is not finished yet
755
- if( $this->options->job->total > $this->options->job->start ) {
756
- return false;
757
- }
758
-
759
- // Add it to cloned tables listing
760
- $this->options->clonedTables[] = $this->options->tables[$this->options->currentStep];
761
-
762
- // Reset job
763
- $this->options->job = new \stdClass();
764
-
765
- return true;
766
- }
767
-
768
- /**
769
- * Drop table if necessary
770
- * @param string $new
771
- */
772
- private function dropTable( $new ) {
773
- $old = $this->db->get_var( $this->db->prepare( "SHOW TABLES LIKE %s", $new ) );
774
-
775
- if( !$this->shouldDropTable( $new, $old ) ) {
776
- return;
777
- }
778
-
779
- $this->log( "DB Processing: {$new} already exists, dropping it first" );
780
- $this->db->query( "DROP TABLE {$new}" );
781
- }
782
-
783
- /**
784
- * Check if table needs to be dropped
785
- * @param string $new
786
- * @param string $old
787
- * @return bool
788
- */
789
- private function shouldDropTable( $new, $old ) {
790
- return (
791
- $old == $new &&
792
- (
793
- !isset( $this->options->job->current ) ||
794
- !isset( $this->options->job->start ) ||
795
- 0 == $this->options->job->start
796
- )
797
- );
798
- }
799
-
800
- /**
801
- * Check if WP is installed in subdir
802
- * @return boolean
803
- */
804
- private function isSubDir() {
805
- // Compare names without scheme to bypass cases where siteurl and home have different schemes http / https
806
- // This is happening much more often than you would expect
807
- $siteurl = preg_replace( '#^https?://#', '', rtrim( get_option( 'siteurl' ), '/' ) );
808
- $home = preg_replace( '#^https?://#', '', rtrim( get_option( 'home' ), '/' ) );
809
-
810
- if( $home !== $siteurl ) {
811
- return true;
812
- }
813
- return false;
814
- }
815
-
816
- }
1
+ <?php
2
+
3
+ namespace WPStaging\Backend\Modules\Jobs\Multisite;
4
+
5
+ // No Direct Access
6
+ if( !defined( "WPINC" ) ) {
7
+ die;
8
+ }
9
+
10
+ use WPStaging\WPStaging;
11
+ use WPStaging\Utils\Strings;
12
+ use WPStaging\Utils\Helper;
13
+ use WPStaging\Utils\Multisite;
14
+ use WPStaging\Backend\Modules\Jobs\JobExecutable;
15
+
16
+ /**
17
+ * Class Database
18
+ * @package WPStaging\Backend\Modules\Jobs
19
+ */
20
+ class SearchReplace extends JobExecutable {
21
+
22
+ /**
23
+ * @var int
24
+ */
25
+ private $total = 0;
26
+
27
+ /**
28
+ * @var \WPDB
29
+ */
30
+ public $db;
31
+
32
+ /**
33
+ *
34
+ * @var Obj
35
+ */
36
+ private $strings;
37
+
38
+ /**
39
+ *
40
+ * @var string
41
+ */
42
+ private $destinationHostname;
43
+
44
+ /**
45
+ *
46
+ * @var string
47
+ */
48
+ private $sourceHostname;
49
+
50
+ /**
51
+ *
52
+ * @var string
53
+ */
54
+ //private $targetDir;
55
+
56
+ /**
57
+ * The prefix of the new database tables which are used for the live site after updating tables
58
+ * @var string
59
+ */
60
+ public $tmpPrefix;
61
+
62
+ /**
63
+ * Initialize
64
+ */
65
+ public function initialize() {
66
+ $this->total = count( $this->options->tables );
67
+ $this->db = WPStaging::getInstance()->get( "wpdb" );
68
+ $this->tmpPrefix = $this->options->prefix;
69
+ $this->strings = new Strings();
70
+ $this->sourceHostname = $this->getSourceHostname();
71
+ $this->destinationHostname = $this->getDestinationHostname();
72
+ }
73
+
74
+ public function start() {
75
+ // Skip job. Nothing to do
76
+ if( $this->options->totalSteps === 0 ) {
77
+ $this->prepareResponse( true, false );
78
+ }
79
+
80
+ $this->run();
81
+
82
+ // Save option, progress
83
+ $this->saveOptions();
84
+
85
+ return ( object ) $this->response;
86
+ }
87
+
88
+ /**
89
+ * Calculate Total Steps in This Job and Assign It to $this->options->totalSteps
90
+ * @return void
91
+ */
92
+ protected function calculateTotalSteps() {
93
+ $this->options->totalSteps = $this->total;
94
+ }
95
+
96
+ /**
97
+ * Execute the Current Step
98
+ * Returns false when over threshold limits are hit or when the job is done, true otherwise
99
+ * @return bool
100
+ */
101
+ protected function execute() {
102
+ // Over limits threshold
103
+ if( $this->isOverThreshold() ) {
104
+ // Prepare response and save current progress
105
+ $this->prepareResponse( false, false );
106
+ $this->saveOptions();
107
+ return false;
108
+ }
109
+
110
+ // No more steps, finished
111
+ if( $this->options->currentStep > $this->total || !isset( $this->options->tables[$this->options->currentStep] ) ) {
112
+ $this->prepareResponse( true, false );
113
+ return false;
114
+ }
115
+
116
+ // Table is excluded
117
+ if( in_array( $this->options->tables[$this->options->currentStep], $this->options->excludedTables ) ) {
118
+ $this->prepareResponse();
119
+ return true;
120
+ }
121
+
122
+ // Search & Replace
123
+ if( !$this->stopExecution() && !$this->updateTable( $this->options->tables[$this->options->currentStep] ) ) {
124
+ // Prepare Response
125
+ $this->prepareResponse( false, false );
126
+
127
+ // Not finished
128
+ return true;
129
+ }
130
+
131
+
132
+ // Prepare Response
133
+ $this->prepareResponse();
134
+
135
+ // Not finished
136
+ return true;
137
+ }
138
+
139
+ /**
140
+ * Stop Execution immediately
141
+ * return mixed bool | json
142
+ */
143
+ private function stopExecution() {
144
+ if( $this->db->prefix == $this->tmpPrefix ) {
145
+ $this->returnException( 'Fatal Error 9: Prefix ' . $this->db->prefix . ' is used for the live site hence it can not be used for the staging site as well. Please ask support@wp-staging.com how to resolve this.' );
146
+ }
147
+ return false;
148
+ }
149
+
150
+ /**
151
+ * Copy Tables
152
+ * @param string $tableName
153
+ * @return bool
154
+ */
155
+ private function updateTable( $tableName ) {
156
+ $strings = new Strings();
157
+ $table = $strings->str_replace_first( $this->db->prefix, '', $tableName );
158
+ $newTableName = $this->tmpPrefix . $table;
159
+
160
+ // Save current job
161
+ $this->setJob( $newTableName );
162
+
163
+ // Beginning of the job
164
+ if( !$this->startJob( $newTableName, $tableName ) ) {
165
+ return true;
166
+ }
167
+ // Copy data
168
+ $this->startReplace( $newTableName );
169
+
170
+ // Finish the step
171
+ return $this->finishStep();
172
+ }
173
+
174
+ /**
175
+ * Get source Hostname depending on wheather WP has been installed in sub dir or not
176
+ * @return type
177
+ */
178
+ public function getSourceHostname() {
179
+
180
+ if( $this->isSubDir() ) {
181
+ return trailingslashit( $this->multisiteHomeUrlWithoutScheme ) . '/' . $this->getSubDir();
182
+ }
183
+ return $this->multisiteHomeUrlWithoutScheme;
184
+ }
185
+
186
+ /**
187
+ * Get destination Hostname depending on wheather WP has been installed in sub dir or not
188
+ * @return type
189
+ */
190
+ public function getDestinationHostname() {
191
+
192
+ if( !empty( $this->options->cloneHostname ) ) {
193
+ return $this->strings->getUrlWithoutScheme( $this->options->cloneHostname );
194
+ }
195
+
196
+ if( $this->isSubDir() ) {
197
+ return trailingslashit( $this->strings->getUrlWithoutScheme( $this->multisiteDomainWithoutScheme ) ) . $this->getSubDir() . '/' . $this->options->cloneDirectoryName;
198
+ }
199
+
200
+ // Get the path to the main multisite without appending and trailingslash e.g. wordpress
201
+ $multisitePath = defined( 'PATH_CURRENT_SITE') ? PATH_CURRENT_SITE : '/';
202
+ $url = rtrim( $this->strings->getUrlWithoutScheme( $this->multisiteDomainWithoutScheme ), '/\\' ) . $multisitePath . $this->options->cloneDirectoryName;
203
+ //$multisitePath = defined( 'PATH_CURRENT_SITE' ) ? str_replace( '/', '', PATH_CURRENT_SITE ) : '';
204
+ //$url = trailingslashit( $this->strings->getUrlWithoutScheme( $this->multisiteDomainWithoutScheme ) ) . $multisitePath . '/' . $this->options->cloneDirectoryName;
205
+ return $url;
206
+ }
207
+
208
+ /**
209
+ * Get the install sub directory if WP is installed in sub directory
210
+ * @return string
211
+ */
212
+ private function getSubDir() {
213
+ $home = get_option( 'home' );
214
+ $siteurl = get_option( 'siteurl' );
215
+
216
+ if( empty( $home ) || empty( $siteurl ) ) {
217
+ return '';
218
+ }
219
+
220
+ $dir = str_replace( $home, '', $siteurl );
221
+ return str_replace( '/', '', $dir );
222
+ }
223
+
224
+ /**
225
+ * Start search replace job
226
+ * @param string $new
227
+ * @param string $old
228
+ */
229
+ private function startReplace( $table ) {
230
+ $rows = $this->options->job->start + $this->settings->querySRLimit;
231
+ $this->log(
232
+ "DB Processing: Table {$table} {$this->options->job->start} to {$rows} records"
233
+ );
234
+
235
+ // Search & Replace
236
+ $this->searchReplace( $table, $rows, array() );
237
+
238
+ // Set new offset
239
+ $this->options->job->start += $this->settings->querySRLimit;
240
+ }
241
+
242
+ /**
243
+ * Returns the number of pages in a table.
244
+ * @access public
245
+ * @return int
246
+ */
247
+ private function get_pages_in_table( $table ) {
248
+
249
+ // Table does not exist
250
+ $result = $this->db->query( "SHOW TABLES LIKE '{$table}'" );
251
+ if( !$result || 0 === $result ) {
252
+ return 0;
253
+ }
254
+
255
+ $table = esc_sql( $table );
256
+ $rows = $this->db->get_var( "SELECT COUNT(*) FROM $table" );
257
+ $pages = ceil( $rows / $this->settings->querySRLimit );
258
+ return absint( $pages );
259
+ }
260
+
261
+ /**
262
+ * Gets the columns in a table.
263
+ * @access public
264
+ * @param string $table The table to check.
265
+ * @return array
266
+ */
267
+ private function get_columns( $table ) {
268
+ $primary_key = null;
269
+ $columns = array();
270
+ $fields = $this->db->get_results( 'DESCRIBE ' . $table );
271
+ if( is_array( $fields ) ) {
272
+ foreach ( $fields as $column ) {
273
+ $columns[] = $column->Field;
274
+ if( $column->Key == 'PRI' ) {
275
+ $primary_key = $column->Field;
276
+ }
277
+ }
278
+ }
279
+ return array($primary_key, $columns);
280
+ }
281
+
282
+ /**
283
+ * Return absolute destination path
284
+ * @return string
285
+ */
286
+ private function getAbsDestination() {
287
+ if( empty( $this->options->cloneDir ) ) {
288
+ return \WPStaging\WPStaging::getWPpath();
289
+ }
290
+ return trailingslashit( $this->options->cloneDir );
291
+ }
292
+
293
+ /**
294
+ * Adapated from interconnect/it's search/replace script, adapted from Better Search Replace
295
+ *
296
+ * Modified to use WordPress wpdb functions instead of PHP's native mysql/pdo functions,
297
+ * and to be compatible with batch processing.
298
+ *
299
+ * @link https://interconnectit.com/products/search-and-replace-for-wordpress-databases/
300
+ *
301
+ * @access public
302
+ * @param string $table The table to run the replacement on.
303
+ * @param int $page The page/block to begin the query on.
304
+ * @param array $args An associative array containing arguments for this run.
305
+ * @return array
306
+ */
307
+ private function searchReplace( $table, $page, $args ) {
308
+
309
+ if( $this->thirdParty->isSearchReplaceExcluded( $table ) ) {
310
+ $this->log( "DB Processing: Skip {$table}", \WPStaging\Utils\Logger::TYPE_INFO );
311
+ return true;
312
+ }
313
+
314
+ // Load up the default settings for this chunk.
315
+ $table = esc_sql( $table );
316
+ $current_page = $this->options->job->start + $this->settings->querySRLimit;
317
+ $pages = $this->get_pages_in_table( $table );
318
+
319
+
320
+ // Search URL example.com/staging and root path to staging site /var/www/htdocs/staging
321
+ $args['search_for'] = array(
322
+ '//' . $this->getSourceHostname(),
323
+ ABSPATH,
324
+ '\/\/' . str_replace( '/', '\/', $this->getSourceHostname() ), // Used by revslider and several visual editors
325
+ '%2F%2F' . str_replace( '/', '%2F', $this->getSourceHostname() ), // HTML entitity for WP Backery Page Builder Plugin
326
+ //$this->getImagePathLive()
327
+ );
328
+
329
+
330
+ $args['replace_with'] = array(
331
+ '//' . $this->getDestinationHostname(),
332
+ $this->options->destinationDir,
333
+ '\/\/' . str_replace( '/', '\/', $this->getDestinationHostname() ), // Used by revslider and several visual editors
334
+ '%2F%2F' . str_replace( '/', '%2F', $this->getDestinationHostname() ), // HTML entitity for WP Backery Page Builder Plugin
335
+ //$this->getImagePathStaging()
336
+ );
337
+
338
+ $this->debugLog( "DB Processing: Search: {$args['search_for'][0]}", \WPStaging\Utils\Logger::TYPE_INFO );
339
+ $this->debugLog( "DB Processing: Replace: {$args['replace_with'][0]}", \WPStaging\Utils\Logger::TYPE_INFO );
340
+
341
+
342
+
343
+ $args['replace_guids'] = 'off';
344
+ $args['dry_run'] = 'off';
345
+ $args['case_insensitive'] = false;
346
+ //$args['replace_mails'] = 'off';
347
+ $args['skip_transients'] = 'on';
348
+
349
+
350
+ // Allow filtering of search & replace parameters
351
+ $args = apply_filters( 'wpstg_clone_searchreplace_params', $args );
352
+
353
+ // Get a list of columns in this table.
354
+ list( $primary_key, $columns ) = $this->get_columns( $table );
355
+
356
+ // Bail out early if there isn't a primary key.
357
+ // We commented this to search & replace through tables which have no primary keys like wp_revslider_slides
358
+ // @todo test this carefully. If it causes (performance) issues we need to activate it again!
359
+ // @since 2.4.4
360
+ // if( null === $primary_key ) {
361
+ // return false;
362
+ // }
363
+
364
+ $current_row = 0;
365
+ $start = $this->options->job->start;
366
+ $end = $this->settings->querySRLimit;
367
+
368
+ // Grab the content of the table.
369
+ $data = $this->db->get_results( "SELECT * FROM $table LIMIT $start, $end", ARRAY_A );
370
+
371
+ // Filter certain rows option_name in wpstg_options
372
+ $filter = array(
373
+ 'Admin_custome_login_Slidshow',
374
+ 'Admin_custome_login_Social',
375
+ 'Admin_custome_login_logo',
376
+ 'Admin_custome_login_text',
377
+ 'Admin_custome_login_login',
378
+ 'Admin_custome_login_top',
379
+ 'Admin_custome_login_dashboard',
380
+ 'Admin_custome_login_Version',
381
+ 'upload_path',
382
+ );
383
+
384
+ $filter = apply_filters( 'wpstg_clone_searchreplace_excl_rows', $filter );
385
+
386
+ // Loop through the data.
387
+ foreach ( $data as $row ) {
388
+ $current_row++;
389
+ $update_sql = array();
390
+ $where_sql = array();
391
+ $upd = false;
392
+
393
+ // Skip rows below
394
+ if( isset( $row['option_name'] ) && in_array( $row['option_name'], $filter ) ) {
395
+ continue;
396
+ }
397
+
398
+ // Skip rows with transients (They can store huge data and we need to save memory)
399
+ if( isset( $row['option_name'] ) && 'on' === $args['skip_transients'] && false !== strpos( $row['option_name'], '_transient' ) ) {
400
+ continue;
401
+ }
402
+ // Skip rows with more than 5MB to save memory
403
+ if( isset( $row['option_value'] ) && strlen( $row['option_value'] ) >= 5000000 ) {
404
+ continue;
405
+ }
406
+
407
+
408
+ foreach ( $columns as $column ) {
409
+
410
+ $dataRow = $row[$column];
411
+
412
+ // Skip rows larger than 5MB
413
+ $size = strlen( $dataRow );
414
+ if( $size >= 5000000 ) {
415
+ continue;
416
+ }
417
+
418
+ // Skip Primary key
419
+ if( $column == $primary_key ) {
420
+ $where_sql[] = $column . ' = "' . $this->mysql_escape_mimic( $dataRow ) . '"';
421
+ continue;
422
+ }
423
+
424
+ // Skip GUIDs by default.
425
+ if( 'on' !== $args['replace_guids'] && 'guid' == $column ) {
426
+ continue;
427
+ }
428
+
429
+ // Skip mail addresses
430
+ // if( 'off' === $args['replace_mails'] && false !== strpos( $dataRow, '@' . $this->multisiteDomainWithoutScheme ) ) {
431
+ // continue;
432
+ // }
433
+ // Check options table
434
+ if( $this->options->prefix . 'options' === $table ) {
435
+
436
+ // Skip certain options
437
+ // if( isset( $should_skip ) && true === $should_skip ) {
438
+ // $should_skip = false;
439
+ // continue;
440
+ // }
441
+ // Skip this row
442
+ if( 'wpstg_existing_clones_beta' === $dataRow ||
443
+ 'wpstg_existing_clones' === $dataRow ||
444
+ 'wpstg_settings' === $dataRow ||
445
+ 'wpstg_license_status' === $dataRow ||
446
+ 'siteurl' === $dataRow ||
447
+ 'home' === $dataRow
448
+ ) {
449
+ //$should_skip = true;
450
+ continue;
451
+ }
452
+ }
453
+
454
+ // Check the path delimiter for / or \/ and remove one of those which prevents from resulting in wrong syntax like domain.com/staging\/.
455
+ // 1. local.wordpress.test -> local.wordpress.test/staging
456
+ // 2. local.wordpress.test\/ -> local.wordpress.test\/staging\/
457
+ $tmp = $args;
458
+ if( false === strpos( $dataRow, $tmp['search_for'][0] ) ) {
459
+ array_shift( $tmp['search_for'] ); // rtrim( $this->homeUrl, '/' ),
460
+ array_shift( $tmp['replace_with'] ); // rtrim( $this->homeUrl, '/' ) . '/' . $this->options->cloneDirectoryName,
461
+ } else {
462
+ unset( $tmp['search_for'][1] );
463
+ unset( $tmp['replace_with'][1] );
464
+ // recount array
465
+ $tmp['search_for'] = array_values( $tmp['search_for'] );
466
+ $tmp['replace_with'] = array_values( $tmp['replace_with'] );
467
+ }
468
+
469
+ // Run a search replace on the data row and respect the serialisation.
470
+ $i = 0;
471
+ foreach ( $tmp['search_for'] as $replace ) {
472
+ $dataRow = $this->recursive_unserialize_replace( $tmp['search_for'][$i], $tmp['replace_with'][$i], $dataRow, false, $args['case_insensitive'] );
473
+ $i++;
474
+ }
475
+ unset( $replace );
476
+ unset( $i );
477
+ unset( $tmp );
478
+
479
+ // Something was changed
480
+ if( $row[$column] != $dataRow ) {
481
+ $update_sql[] = $column . ' = "' . $this->mysql_escape_mimic( $dataRow ) . '"';
482
+ $upd = true;
483
+ }
484
+ }
485
+
486
+ // Determine what to do with updates.
487
+ if( $args['dry_run'] === 'on' ) {
488
+ // Don't do anything if a dry run
489
+ } elseif( $upd && !empty( $where_sql ) ) {
490
+ // If there are changes to make, run the query.
491
+ $sql = 'UPDATE ' . $table . ' SET ' . implode( ', ', $update_sql ) . ' WHERE ' . implode( ' AND ', array_filter( $where_sql ) );
492
+ $result = $this->db->query( $sql );
493
+
494
+ if( !$result ) {
495
+ $this->log( "Error updating row {$current_row} SQL: {$sql}", \WPStaging\Utils\Logger::TYPE_ERROR );
496
+ }
497
+ }
498
+ } // end row loop
499
+ unset( $row );
500
+ unset( $update_sql );
501
+ unset( $where_sql );
502
+ unset( $sql );
503
+
504
+
505
+ // DB Flush
506
+ $this->db->flush();
507
+ return true;
508
+ }
509
+
510
+ /**
511
+ * Get path to multisite image folder e.g. wp-content/blogs.dir/ID/files or wp-content/uploads/sites/ID
512
+ * @return string
513
+ */
514
+ private function getImagePathLive() {
515
+ // Check first which structure is used
516
+ $uploads = wp_upload_dir();
517
+ $basedir = $uploads['basedir'];
518
+ $blogId = get_current_blog_id();
519
+
520
+ if( false === strpos( $basedir, 'blogs.dir' ) ) {
521
+ // Since WP 3.5
522
+ $path = $blogId > 1 ?
523
+ 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'sites' . DIRECTORY_SEPARATOR . get_current_blog_id() . DIRECTORY_SEPARATOR :
524
+ 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR;
525
+ } else {
526
+ // old blog structure
527
+ $path = $blogId > 1 ?
528
+ 'wp-content' . DIRECTORY_SEPARATOR . 'blogs.dir' . DIRECTORY_SEPARATOR . get_current_blog_id() . DIRECTORY_SEPARATOR . 'files' . DIRECTORY_SEPARATOR :
529
+ 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR;
530
+ }
531
+ return $path;
532
+ }
533
+
534
+ /**
535
+ * Get path to staging site image path wp-content/uploads
536
+ * @return string
537
+ */
538
+ private function getImagePathStaging() {
539
+ return 'wp-content' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR;
540
+ }
541
+
542
+ /**
543
+ * Adapted from interconnect/it's search/replace script.
544
+ *
545
+ * @link https://interconnectit.com/products/search-and-replace-for-wordpress-databases/
546
+ *
547
+ * Take a serialised array and unserialise it replacing elements as needed and
548
+ * unserialising any subordinate arrays and performing the replace on those too.
549
+ *
550
+ * @access private
551
+ * @param string $from String we're looking to replace.
552
+ * @param string $to What we want it to be replaced with
553
+ * @param array $data Used to pass any subordinate arrays back to in.
554
+ * @param boolean $serialized Does the array passed via $data need serialising.
555
+ * @param sting|boolean $case_insensitive Set to 'on' if we should ignore case, false otherwise.
556
+ *
557
+ * @return string|array The original array with all elements replaced as needed.
558
+ */
559
+ private function recursive_unserialize_replace( $from = '', $to = '', $data = '', $serialized = false, $case_insensitive = false ) {
560
+ try {
561
+ // Some unserialized data cannot be re-serialized eg. SimpleXMLElements
562
+ if( is_serialized( $data ) && ( $unserialized = @unserialize( $data ) ) !== false ) {
563
+ $data = $this->recursive_unserialize_replace( $from, $to, $unserialized, true, $case_insensitive );
564
+ } elseif( is_array( $data ) ) {
565
+ $tmp = array();
566
+ foreach ( $data as $key => $value ) {
567
+ $tmp[$key] = $this->recursive_unserialize_replace( $from, $to, $value, false, $case_insensitive );
568
+ }
569
+
570
+ $data = $tmp;
571
+ unset( $tmp );
572
+ } elseif( is_object( $data ) ) {
573
+ $tmp = $data;
574
+ $props = get_object_vars( $data );
575
+
576
+ // Do not continue if class contains __PHP_Incomplete_Class_Name
577
+ if( !empty( $props['__PHP_Incomplete_Class_Name'] ) ) {
578
+ return $data;
579
+
580
+ }
581
+
582
+ // Do a search & replace
583
+ foreach ( $props as $key => $value ) {
584
+ if( $key === '' || ord( $key[0] ) === 0 ) {
585
+ continue;
586
+ }
587
+ $tmp->$key = $this->recursive_unserialize_replace( $from, $to, $value, false, $case_insensitive );
588
+ }
589
+
590
+ $data = $tmp;
591
+ unset( $tmp );
592
+ unset( $props );
593
+ } else {
594
+ if( is_string( $data ) ) {
595
+ if( !empty( $from ) && !empty( $to ) ) {
596
+ $data = $this->str_replace( $from, $to, $data, $case_insensitive );
597
+ }
598
+ }
599
+ }
600
+
601
+ if( $serialized ) {
602
+ return serialize( $data );
603
+ }
604
+ } catch ( Exception $error ) {
605
+
606
+ }
607
+
608
+ return $data;
609
+ }
610
+
611
+ /**
612
+ * Check if the object is a valid one and not __PHP_Incomplete_Class_Name
613
+ * Can not use is_object alone because in php 7.2 it's returning true even though object is __PHP_Incomplete_Class_Name
614
+ * @return boolean
615
+ */
616
+ // private function isValidObject( $data ) {
617
+ // if( !is_object( $data ) || gettype( $data ) != 'object' ) {
618
+ // return false;
619
+ // }
620
+ //
621
+ // $invalid_class_props = get_object_vars( $data );
622
+ //
623
+ // if (!isset($invalid_class_props['__PHP_Incomplete_Class_Name'])){
624
+ // // Assume it must be an valid object
625
+ // return true;
626
+ // }
627
+ //
628
+ // $invalid_object_class = $invalid_class_props['__PHP_Incomplete_Class_Name'];
629
+ //
630
+ // if( !empty( $invalid_object_class ) ) {
631
+ // return false;
632
+ // }
633
+ //
634
+ // // Assume it must be an valid object
635
+ // return true;
636
+ // }
637
+
638
+ /**
639
+ * Mimics the mysql_real_escape_string function. Adapted from a post by 'feedr' on php.net.
640
+ * @link http://php.net/manual/en/function.mysql-real-escape-string.php#101248
641
+ * @access public
642
+ * @param string $input The string to escape.
643
+ * @return string
644
+ */
645
+ private function mysql_escape_mimic( $input ) {
646
+ if( is_array( $input ) ) {
647
+ return array_map( __METHOD__, $input );
648
+ }
649
+ if( !empty( $input ) && is_string( $input ) ) {
650
+ return str_replace( array('\\', "\0", "\n", "\r", "'", '"', "\x1a"), array('\\\\', '\\0', '\\n', '\\r', "\\'", '\\"', '\\Z'), $input );
651
+ }
652
+
653
+ return $input;
654
+ }
655
+
656
+ /**
657
+ * Return unserialized object or array
658
+ *
659
+ * @param string $serialized_string Serialized string.
660
+ * @param string $method The name of the caller method.
661
+ *
662
+ * @return mixed, false on failure
663
+ */
664
+ private static function unserialize( $serialized_string ) {
665
+ if( !is_serialized( $serialized_string ) ) {
666
+ return false;
667
+ }
668
+
669
+ $serialized_string = trim( $serialized_string );
670
+ $unserialized_string = @unserialize( $serialized_string );
671
+
672
+ return $unserialized_string;
673
+ }
674
+
675
+ /**
676
+ * Wrapper for str_replace
677
+ *
678
+ * @param string $from
679
+ * @param string $to
680
+ * @param string $data
681
+ * @param string|bool $case_insensitive
682
+ *
683
+ * @return string
684
+ */
685
+ private function str_replace( $from, $to, $data, $case_insensitive = false ) {
686
+
687
+ // Add filter
688
+ $excludes = apply_filters( 'wpstg_clone_searchreplace_excl', array() );
689
+
690
+ // Build pattern
691
+ $regexExclude = '';
692
+ foreach ( $excludes as $exclude ) {
693
+ $regexExclude .= $exclude . '(*SKIP)(FAIL)|';
694
+ }
695
+
696
+ if( 'on' === $case_insensitive ) {
697
+ //$data = str_ireplace( $from, $to, $data );
698
+ $data = preg_replace( '#' . $regexExclude . preg_quote( $from ) . '#i', $to, $data );
699
+ } else {
700
+ //$data = str_replace( $from, $to, $data );
701
+ $data = preg_replace( '#' . $regexExclude . preg_quote( $from ) . '#', $to, $data );
702
+ }
703
+
704
+ return $data;
705
+ }
706
+
707
+ /**
708
+ * Set the job
709
+ * @param string $table
710
+ */
711
+ private function setJob( $table ) {
712
+ if( !empty( $this->options->job->current ) ) {
713
+ return;
714
+ }
715
+
716
+ $this->options->job->current = $table;
717
+ $this->options->job->start = 0;
718
+ }
719
+
720
+ /**
721
+ * Start Job
722
+ * @param string $new
723
+ * @param string $old
724
+ * @return bool
725
+ */
726
+ private function startJob( $new, $old ) {
727
+
728
+ $this->options->job->total = 0;
729
+
730
+ if( 0 != $this->options->job->start ) {
731
+ return true;
732
+ }
733
+
734
+ // Table does not exist
735
+ $result = $this->db->query( "SHOW TABLES LIKE '{$old}'" );
736
+ if( !$result || 0 === $result ) {
737
+ return false;
738
+ }
739
+
740
+ $this->options->job->total = ( int ) $this->db->get_var( "SELECT COUNT(1) FROM {$old}" );
741
+
742
+ if( 0 == $this->options->job->total ) {
743
+ $this->finishStep();
744
+ return false;
745
+ }
746
+
747
+ return true;
748
+ }
749
+
750
+ /**
751
+ * Finish the step
752
+ */
753
+ private function finishStep() {
754
+ // This job is not finished yet
755
+ if( $this->options->job->total > $this->options->job->start ) {
756
+ return false;
757
+ }
758
+
759
+ // Add it to cloned tables listing
760
+ $this->options->clonedTables[] = $this->options->tables[$this->options->currentStep];
761
+
762
+ // Reset job
763
+ $this->options->job = new \stdClass();
764
+
765
+ return true;
766
+ }
767
+
768
+ /**
769
+ * Drop table if necessary
770
+ * @param string $new
771
+ */
772
+ private function dropTable( $new ) {
773
+ $old = $this->db->get_var( $this->db->prepare( "SHOW TABLES LIKE %s", $new ) );
774
+
775
+ if( !$this->shouldDropTable( $new, $old ) ) {
776
+ return;
777
+ }
778
+
779
+ $this->log( "DB Processing: {$new} already exists, dropping it first" );
780
+ $this->db->query( "DROP TABLE {$new}" );
781
+ }
782
+
783
+ /**
784
+ * Check if table needs to be dropped
785
+ * @param string $new
786
+ * @param string $old
787
+ * @return bool
788
+ */
789
+ private function shouldDropTable( $new, $old ) {
790
+ return (
791
+ $old == $new &&
792
+ (
793
+ !isset( $this->options->job->current ) ||
794
+ !isset( $this->options->job->start ) ||
795
+ 0 == $this->options->job->start
796
+ )
797
+ );
798
+ }
799
+
800
+ /**
801
+ * Check if WP is installed in subdir
802
+ * @return boolean
803
+ */
804
+ private function isSubDir() {
805
+ // Compare names without scheme to bypass cases where siteurl and home have different schemes http / https
806
+ // This is happening much more often than you would expect
807
+ $siteurl = preg_replace( '#^https?://#', '', rtrim( get_option( 'siteurl' ), '/' ) );
808
+ $home = preg_replace( '#^https?://#', '', rtrim( get_option( 'home' ), '/' ) );
809
+
810
+ if( $home !== $siteurl ) {
811
+ return true;
812
+ }
813
+ return false;
814
+ }
815
+
816
+ }
apps/Backend/Modules/Jobs/Multisite/SearchReplaceExternal.php CHANGED
@@ -1,840 +1,840 @@
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 exist
252
- $table = str_replace( $this->options->prefix . '.', null, $table );
253
- $result = $this->productionDb->query( "SHOW TABLES LIKE '{$table}'" );
254
- if( !$result || 0 === $result ) {
255
- return 0;
256
- }
257
-
258
- $table = esc_sql( $table );
259
- $rows = $this->productionDb->get_var( "SELECT COUNT(*) FROM $table" );
260
- $pages = ceil( $rows / $this->settings->querySRLimit );
261
- return absint( $pages );
262
- }
263
-
264
- /**
265
- * Gets the columns in a table.
266
- * @access public
267
- * @param string $table The table to check.
268
- * @return array
269
- */
270
- private function get_columns( $table ) {
271
- $primary_key = null;
272
- $columns = array();
273
- $fields = $this->stagingDb->get_results( 'DESCRIBE ' . $table );
274
- if( is_array( $fields ) ) {
275
- foreach ( $fields as $column ) {
276
- $columns[] = $column->Field;
277
- if( $column->Key == 'PRI' ) {
278
- $primary_key = $column->Field;
279
- }
280
- }
281
- }
282
- return array($primary_key, $columns);
283
- }
284
-
285
- /**
286
- * 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