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
- }
316
-
317
- // Load up the default settings for this chunk.
318
- $table = esc_sql( $table );
319
- $current_page = $this->options->job->start + $this->settings->querySRLimit;
320
- $pages = $this->get_pages_in_table( $table );
321
-
322
-
323
- // Search URL example.com/staging and root path to staging site /var/www/htdocs/staging
324
- $args['search_for'] = array(
325
- '//' . $this->sourceHostname,
326
- ABSPATH,
327
- '\/\/' . str_replace( '/', '\/', $this->sourceHostname ), // Used by revslider and several visual editors
328
- '%2F%2F' . str_replace( '/', '%2F', $this->sourceHostname ), // HTML entitity for WP Backery Page Builder Plugin
329
- //$this->getImagePathLive()
330
- );
331
-
332
-
333
- $args['replace_with'] = array(
334
- '//' . $this->destinationHostname,
335
- $this->options->destinationDir,
336
- '\/\/' . str_replace( '/', '\/', $this->destinationHostname ), // Used by revslider and several visual editors
337
- '%2F%2F' . str_replace( '/', '%2F', $this->destinationHostname ), // HTML entitity for WP Backery Page Builder Plugin
338
- //$this->getImagePathStaging()
339
- );
340
-
341
-
342
- $args['replace_guids'] = 'off';
343
- $args['dry_run'] = 'off';
344
- $args['case_insensitive'] = false;
345
- //$args['replace_mails'] = 'off';
346
- $args['skip_transients'] = 'on';
347
-
348
-
349
- // Allow filtering of search & replace parameters
350
- $args = apply_filters( 'wpstg_clone_searchreplace_params', $args );
351
-
352
- // Get a list of columns in this table.
353
- list( $primary_key, $columns ) = $this->get_columns( $table );
354
-
355
- // Bail out early if there isn't a primary key.
356
- // We commented this to search & replace through tables which have no primary keys like wp_revslider_slides
357
- // @todo test this carefully. If it causes (performance) issues we need to activate it again!
358
- // @since 2.4.4
359
- // if( null === $primary_key ) {
360
- // return false;
361
- // }
362
-
363
- $current_row = 0;
364
- $start = $this->options->job->start;
365
- $end = $this->settings->querySRLimit;
366
-
367
- // Grab the content of the table.
368
- $data = $this->stagingDb->get_results( "SELECT * FROM $table LIMIT $start, $end", ARRAY_A );
369
-
370
- // Filter certain rows (of other plugins)
371
- $filter = array(
372
- 'Admin_custome_login_Slidshow',
373
- 'Admin_custome_login_Social',
374
- 'Admin_custome_login_logo',
375
- 'Admin_custome_login_text',
376
- 'Admin_custome_login_login',
377
- 'Admin_custome_login_top',
378
- 'Admin_custome_login_dashboard',
379
- 'Admin_custome_login_Version',
380
- 'upload_path',
381
- );
382
-
383
- $filter = apply_filters( 'wpstg_clone_searchreplace_excl_rows', $filter );
384
-
385
- // Loop through the data.
386
- foreach ( $data as $row ) {
387
- $current_row++;
388
- $update_sql = array();
389
- $where_sql = array();
390
- $upd = false;
391
-
392
- // Skip rows below
393
- if( isset( $row['option_name'] ) && in_array( $row['option_name'], $filter ) ) {
394
- continue;
395
- }
396
-
397
- // Skip rows with transients (They can store huge data and we need to save memory)
398
- if( isset( $row['option_name'] ) && 'on' === $args['skip_transients'] && false !== strpos( $row['option_name'], '_transient' ) ) {
399
- continue;
400
- }
401
- // Skip rows with more than 5MB to save memory
402
- if( isset( $row['option_value'] ) && strlen( $row['option_value'] ) >= 5000000 ) {
403
- continue;
404
- }
405
-
406
-
407
- foreach ( $columns as $column ) {
408
-
409
- $dataRow = $row[$column];
410
-
411
- // Skip rows larger than 10MB
412
- $size = strlen( $dataRow );
413
- if( $size >= 5000000 ) {
414
- continue;
415
- }
416
-
417
- // Skip Primary key
418
- if( $column == $primary_key ) {
419
- $where_sql[] = $column . ' = "' . $this->mysql_escape_mimic( $dataRow ) . '"';
420
- continue;
421
- }
422
-
423
- // Skip GUIDs by default.
424
- if( 'on' !== $args['replace_guids'] && 'guid' == $column ) {
425
- continue;
426
- }
427
-
428
- // Skip mail addresses
429
- // if( 'off' === $args['replace_mails'] && false !== strpos( $dataRow, '@' . $this->multisiteDomainWithoutScheme ) ) {
430
- // continue;
431
- // }
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
-
442
- // Skip this row
443
- if( 'wpstg_existing_clones_beta' === $dataRow ||
444
- 'wpstg_existing_clones' === $dataRow ||
445
- 'wpstg_settings' === $dataRow ||
446
- 'wpstg_license_status' === $dataRow ||
447
- 'siteurl' === $dataRow ||
448
- 'home' === $dataRow
449
- ) {
450
- $should_skip = true;
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->stagingDb->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->stagingDb->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
- // Do a search & replace
582
- foreach ( $props as $key => $value ) {
583
- if( $key === '' || ord( $key[0] ) === 0 ) {
584
- continue;
585
- }
586
- $tmp->$key = $this->recursive_unserialize_replace( $from, $to, $value, false, $case_insensitive );
587
- }
588
-
589
- $data = $tmp;
590
- unset( $tmp );
591
- unset( $props );
592
- } else {
593
- if( is_string( $data ) ) {
594
- if( !empty( $from ) && !empty( $to ) ) {
595
- $data = $this->str_replace( $from, $to, $data, $case_insensitive );
596
- }
597
- }
598
- }
599
-
600
- if( $serialized ) {
601
- return serialize( $data );
602
- }
603
- } catch ( Exception $error ) {
604
-
605
- }
606
-
607
- return $data;
608
- }
609
-
610
- /**
611
- * Check if the object is a valid one and not __PHP_Incomplete_Class_Name
612
- * Can not use is_object alone because in php 7.2 it's returning true even though object is __PHP_Incomplete_Class_Name
613
- * @return boolean
614
- */
615
- // private function isValidObject( $data ) {
616
- // if( !is_object( $data ) || gettype( $data ) != 'object' ) {
617
- // return false;
618
- // }
619
- //
620
- // $invalid_class_props = get_object_vars( $data );
621
- //
622
- // if (!isset($invalid_class_props['__PHP_Incomplete_Class_Name'])){
623
- // // Assume it must be an valid object
624
- // return true;
625
- // }
626
- //
627
- // $invalid_object_class = $invalid_class_props['__PHP_Incomplete_Class_Name'];
628
- //
629
- // if( !empty( $invalid_object_class ) ) {
630
- // return false;
631
- // }
632
- //
633
- // // Assume it must be an valid object
634
- // return true;
635
- // }
636
-
637
- /**
638
- * Mimics the mysql_real_escape_string function. Adapted from a post by 'feedr' on php.net.
639
- * @link http://php.net/manual/en/function.mysql-real-escape-string.php#101248
640
- * @access public
641
- * @param string $input The string to escape.
642
- * @return string
643
- */
644
- private function mysql_escape_mimic( $input ) {
645
- if( is_array( $input ) ) {
646
- return array_map( __METHOD__, $input );
647
- }
648
- if( !empty( $input ) && is_string( $input ) ) {
649
- return str_replace( array('\\', "\0", "\n", "\r", "'", '"', "\x1a"), array('\\\\', '\\0', '\\n', '\\r', "\\'", '\\"', '\\Z'), $input );
650
- }
651
-
652
- return $input;
653
- }
654
-
655
- /**
656
- * Return unserialized object or array
657
- *
658
- * @param string $serialized_string Serialized string.
659
- * @param string $method The name of the caller method.
660
- *
661
- * @return mixed, false on failure
662
- */
663
- private static function unserialize( $serialized_string ) {
664
- if( !is_serialized( $serialized_string ) ) {
665
- return false;
666
- }
667
-
668
- $serialized_string = trim( $serialized_string );
669
- $unserialized_string = @unserialize( $serialized_string );
670
-
671
- return $unserialized_string;
672
- }
673
-
674
- /**
675
- * Wrapper for str_replace
676
- *
677
- * @param string $from
678
- * @param string $to
679
- * @param string $data
680
- * @param string|bool $case_insensitive
681
- *
682
- * @return string
683
- */
684
- private function str_replace( $from, $to, $data, $case_insensitive = false ) {
685
-
686
- // Add filter
687
- $excludes = apply_filters( 'wpstg_clone_searchreplace_excl', array() );
688
-
689
- // Build pattern
690
- $regexExclude = '';
691
- foreach ( $excludes as $exclude ) {
692
- $regexExclude .= $exclude . '(*SKIP)(FAIL)|';
693
- }
694
-
695
- if( 'on' === $case_insensitive ) {
696
- //$data = str_ireplace( $from, $to, $data );
697
- $data = preg_replace( '#' . $regexExclude . preg_quote( $from ) . '#i', $to, $data );
698
- } else {
699
- //$data = str_replace( $from, $to, $data );
700
- $data = preg_replace( '#' . $regexExclude . preg_quote( $from ) . '#', $to, $data );
701
- }
702
-
703
- return $data;
704
- }
705
-
706
- /**
707
- * Set the job
708
- * @param string $table
709
- */
710
- private function setJob( $table ) {
711
- if( !empty( $this->options->job->current ) ) {
712
- return;
713
- }
714
-
715
- $this->options->job->current = $table;
716
- $this->options->job->start = 0;
717
- }
718
-
719
- /**
720
- * Start Job
721
- * @param string $new
722
- * @param string $old
723
- * @return bool
724
- */
725
- private function startJob( $new, $old ) {
726
-
727
- if( $this->isExcludedTable( $new ) ) {
728
- return false;
729
- }
730
-
731
- // Table does not exist
732
- $result = $this->productionDb->query( "SHOW TABLES LIKE '{$old}'" );
733
- if( !$result || 0 === $result ) {
734
- return false;
735
- }
736
-
737
- if( 0 != $this->options->job->start ) {
738
- return true;
739
- }
740
-
741
- $this->options->job->total = ( int ) $this->productionDb->get_var( "SELECT COUNT(1) FROM {$old}" );
742
-
743
- if( 0 == $this->options->job->total ) {
744
- $this->finishStep();
745
- return false;
746
- }
747
-
748
- return true;
749
- }
750
-
751
- /**
752
- * Is table excluded from search replace processing?
753
- * @param string $table
754
- * @return boolean
755
- */
756
- private function isExcludedTable( $table ) {
757
-
758
- $customTables = apply_filters( 'wpstg_clone_searchreplace_tables_exclude', array() );
759
- $defaultTables = array('blogs');
760
-
761
- $tables = array_merge( $customTables, $defaultTables );
762
-
763
- $excludedTables = array();
764
- foreach ( $tables as $key => $value ) {
765
- $excludedTables[] = $this->options->prefix . $value;
766
- }
767
-
768
- if( in_array( $table, $excludedTables ) ) {
769
- return true;
770
- }
771
- return false;
772
- }
773
-
774
- /**
775
- * Finish the step
776
- */
777
- private function finishStep() {
778
- // This job is not finished yet
779
- if( $this->options->job->total > $this->options->job->start ) {
780
- return false;
781
- }
782
-
783
- // Add it to cloned tables listing
784
- $this->options->clonedTables[] = $this->options->tables[$this->options->currentStep];
785
-
786
- // Reset job
787
- $this->options->job = new \stdClass();
788
-
789
- return true;
790
- }
791
-
792
- /**
793
- * Drop table if necessary
794
- * @param string $new
795
- */
796
- private function dropTable( $new ) {
797
- $old = $this->stagingDb->get_var( $this->stagingDb->prepare( "SHOW TABLES LIKE %s", $new ) );
798
-
799
- if( !$this->shouldDropTable( $new, $old ) ) {
800
- return;
801
- }
802
-
803
- $this->log( "DB Processing: {$new} already exists, dropping it first" );
804
- $this->stagingDb->query( "DROP TABLE {$new}" );
805
- }
806
-
807
- /**
808
- * Check if table needs to be dropped
809
- * @param string $new
810
- * @param string $old
811
- * @return bool
812
- */
813
- private function shouldDropTable( $new, $old ) {
814
- return (
815
- $old == $new &&
816
- (
817
- !isset( $this->options->job->current ) ||
818
- !isset( $this->options->job->start ) ||
819
- 0 == $this->options->job->start
820
- )
821
- );
822
- }
823
-
824
- /**
825
- * Check if WP is installed in subdir
826
- * @return boolean
827
- */
828
- private function isSubDir() {
829
- // Compare names without scheme to bypass cases where siteurl and home have different schemes http / https
830
- // This is happening much more often than you would expect
831
- $siteurl = preg_replace( '#^https?://#', '', rtrim( get_option( 'siteurl' ), '/' ) );
832
- $home = preg_replace( '#^https?://#', '', rtrim( get_option( 'home' ), '/' ) );
833
-
834
- if( $home !== $siteurl ) {
835
- return true;
836
- }
837
- return false;
838
- }
839
-
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
+ }
316
+
317
+ // Load up the default settings for this chunk.
318
+ $table = esc_sql( $table );
319
+ $current_page = $this->options->job->start + $this->settings->querySRLimit;
320
+ $pages = $this->get_pages_in_table( $table );
321
+
322
+
323
+ // Search URL example.com/staging and root path to staging site /var/www/htdocs/staging
324
+ $args['search_for'] = array(
325
+ '//' . $this->sourceHostname,
326
+ ABSPATH,
327
+ '\/\/' . str_replace( '/', '\/', $this->sourceHostname ), // Used by revslider and several visual editors
328
+ '%2F%2F' . str_replace( '/', '%2F', $this->sourceHostname ), // HTML entitity for WP Backery Page Builder Plugin
329
+ //$this->getImagePathLive()
330
+ );
331
+
332
+
333
+ $args['replace_with'] = array(
334
+ '//' . $this->destinationHostname,
335
+ $this->options->destinationDir,
336
+ '\/\/' . str_replace( '/', '\/', $this->destinationHostname ), // Used by revslider and several visual editors
337
+ '%2F%2F' . str_replace( '/', '%2F', $this->destinationHostname ), // HTML entitity for WP Backery Page Builder Plugin
338
+ //$this->getImagePathStaging()
339
+ );
340
+
341
+
342
+ $args['replace_guids'] = 'off';
343
+ $args['dry_run'] = 'off';
344
+ $args['case_insensitive'] = false;
345
+ //$args['replace_mails'] = 'off';
346
+ $args['skip_transients'] = 'on';
347
+
348
+
349
+ // Allow filtering of search & replace parameters
350
+ $args = apply_filters( 'wpstg_clone_searchreplace_params', $args );
351
+
352
+ // Get a list of columns in this table.
353
+ list( $primary_key, $columns ) = $this->get_columns( $table );
354
+
355
+ // Bail out early if there isn't a primary key.
356
+ // We commented this to search & replace through tables which have no primary keys like wp_revslider_slides
357
+ // @todo test this carefully. If it causes (performance) issues we need to activate it again!
358
+ // @since 2.4.4
359
+ // if( null === $primary_key ) {
360
+ // return false;
361
+ // }
362
+
363
+ $current_row = 0;
364
+ $start = $this->options->job->start;
365
+ $end = $this->settings->querySRLimit;
366
+
367
+ // Grab the content of the table.
368
+ $data = $this->stagingDb->get_results( "SELECT * FROM $table LIMIT $start, $end", ARRAY_A );
369
+
370
+ // Filter certain rows (of other plugins)
371
+ $filter = array(
372
+ 'Admin_custome_login_Slidshow',
373
+ 'Admin_custome_login_Social',
374
+ 'Admin_custome_login_logo',
375
+ 'Admin_custome_login_text',
376
+ 'Admin_custome_login_login',
377
+ 'Admin_custome_login_top',
378
+ 'Admin_custome_login_dashboard',
379
+ 'Admin_custome_login_Version',
380
+ 'upload_path',
381
+ );
382
+
383
+ $filter = apply_filters( 'wpstg_clone_searchreplace_excl_rows', $filter );
384
+
385
+ // Loop through the data.
386
+ foreach ( $data as $row ) {
387
+ $current_row++;
388
+ $update_sql = array();
389
+ $where_sql = array();
390
+ $upd = false;
391
+
392
+ // Skip rows below
393
+ if( isset( $row['option_name'] ) && in_array( $row['option_name'], $filter ) ) {
394
+ continue;
395
+ }
396
+
397
+ // Skip rows with transients (They can store huge data and we need to save memory)
398
+ if( isset( $row['option_name'] ) && 'on' === $args['skip_transients'] && false !== strpos( $row['option_name'], '_transient' ) ) {
399
+ continue;
400
+ }
401
+ // Skip rows with more than 5MB to save memory
402
+ if( isset( $row['option_value'] ) && strlen( $row['option_value'] ) >= 5000000 ) {
403
+ continue;
404
+ }
405
+
406
+
407
+ foreach ( $columns as $column ) {
408
+
409
+ $dataRow = $row[$column];
410
+
411
+ // Skip rows larger than 10MB
412
+ $size = strlen( $dataRow );
413
+ if( $size >= 5000000 ) {
414
+ continue;
415
+ }
416
+
417
+ // Skip Primary key
418
+ if( $column == $primary_key ) {
419
+ $where_sql[] = $column . ' = "' . $this->mysql_escape_mimic( $dataRow ) . '"';
420
+ continue;
421
+ }
422
+
423
+ // Skip GUIDs by default.
424
+ if( 'on' !== $args['replace_guids'] && 'guid' == $column ) {
425
+ continue;
426
+ }
427
+
428
+ // Skip mail addresses
429
+ // if( 'off' === $args['replace_mails'] && false !== strpos( $dataRow, '@' . $this->multisiteDomainWithoutScheme ) ) {
430
+ // continue;
431
+ // }
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
+
442
+ // Skip this row
443
+ if( 'wpstg_existing_clones_beta' === $dataRow ||
444
+ 'wpstg_existing_clones' === $dataRow ||
445
+ 'wpstg_settings' === $dataRow ||
446
+ 'wpstg_license_status' === $dataRow ||
447
+ 'siteurl' === $dataRow ||
448
+ 'home' === $dataRow
449
+ ) {
450
+ $should_skip = true;
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->stagingDb->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->stagingDb->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
+ // Do a search & replace
582
+ foreach ( $props as $key => $value ) {
583
+ if( $key === '' || ord( $key[0] ) === 0 ) {
584
+ continue;
585
+ }
586
+ $tmp->$key = $this->recursive_unserialize_replace( $from, $to, $value, false, $case_insensitive );
587
+ }
588
+
589
+ $data = $tmp;
590
+ unset( $tmp );
591
+ unset( $props );
592
+ } else {
593
+ if( is_string( $data ) ) {
594
+ if( !empty( $from ) && !empty( $to ) ) {
595
+ $data = $this->str_replace( $from, $to, $data, $case_insensitive );
596
+ }
597
+ }
598
+ }
599
+
600
+ if( $serialized ) {
601
+ return serialize( $data );
602
+ }
603
+ } catch ( Exception $error ) {
604
+
605
+ }
606
+
607
+ return $data;
608
+ }
609
+
610
+ /**
611
+ * Check if the object is a valid one and not __PHP_Incomplete_Class_Name
612
+ * Can not use is_object alone because in php 7.2 it's returning true even though object is __PHP_Incomplete_Class_Name
613
+ * @return boolean
614
+ */
615
+ // private function isValidObject( $data ) {
616
+ // if( !is_object( $data ) || gettype( $data ) != 'object' ) {
617
+ // return false;
618
+ // }
619
+ //
620
+ // $invalid_class_props = get_object_vars( $data );
621
+ //
622
+ // if (!isset($invalid_class_props['__PHP_Incomplete_Class_Name'])){
623
+ // // Assume it must be an valid object
624
+ // return true;
625
+ // }
626
+ //
627
+ // $invalid_object_class = $invalid_class_props['__PHP_Incomplete_Class_Name'];
628
+ //
629
+ // if( !empty( $invalid_object_class ) ) {
630
+ // return false;
631
+ // }
632
+ //
633
+ // // Assume it must be an valid object
634
+ // return true;
635
+ // }
636
+
637
+ /**
638
+ * Mimics the mysql_real_escape_string function. Adapted from a post by 'feedr' on php.net.
639
+ * @link http://php.net/manual/en/function.mysql-real-escape-string.php#101248
640
+ * @access public
641
+ * @param string $input The string to escape.
642
+ * @return string
643
+ */
644
+ private function mysql_escape_mimic( $input ) {
645
+ if( is_array( $input ) ) {
646
+ return array_map( __METHOD__, $input );
647
+ }
648
+ if( !empty( $input ) && is_string( $input ) ) {
649
+ return str_replace( array('\\', "\0", "\n", "\r", "'", '"', "\x1a"), array('\\\\', '\\0', '\\n', '\\r', "\\'", '\\"', '\\Z'), $input );
650
+ }
651
+
652
+ return $input;
653
+ }
654
+
655
+ /**
656
+ * Return unserialized object or array
657
+ *
658
+ * @param string $serialized_string Serialized string.
659
+ * @param string $method The name of the caller method.
660
+ *
661
+ * @return mixed, false on failure
662
+ */
663
+ private static function unserialize( $serialized_string ) {
664
+ if( !is_serialized( $serialized_string ) ) {
665
+ return false;
666
+ }
667
+
668
+ $serialized_string = trim( $serialized_string );
669
+ $unserialized_string = @unserialize( $serialized_string );
670
+
671
+ return $unserialized_string;
672
+ }
673
+
674
+ /**
675
+ * Wrapper for str_replace
676
+ *
677
+ * @param string $from
678
+ * @param string $to
679
+ * @param string $data
680
+ * @param string|bool $case_insensitive
681
+ *
682
+ * @return string
683
+ */
684
+ private function str_replace( $from, $to, $data, $case_insensitive = false ) {
685
+
686
+ // Add filter
687
+ $excludes = apply_filters( 'wpstg_clone_searchreplace_excl', array() );
688
+
689
+ // Build pattern
690
+ $regexExclude = '';
691
+ foreach ( $excludes as $exclude ) {
692
+ $regexExclude .= $exclude . '(*SKIP)(FAIL)|';
693
+ }
694
+
695
+ if( 'on' === $case_insensitive ) {
696
+ //$data = str_ireplace( $from, $to, $data );
697
+ $data = preg_replace( '#' . $regexExclude . preg_quote( $from ) . '#i', $to, $data );
698
+ } else {
699
+ //$data = str_replace( $from, $to, $data );
700
+ $data = preg_replace( '#' . $regexExclude . preg_quote( $from ) . '#', $to, $data );
701
+ }
702
+
703
+ return $data;
704
+ }
705
+
706
+ /**
707
+ * Set the job
708
+ * @param string $table
709
+ */
710
+ private function setJob( $table ) {
711
+ if( !empty( $this->options->job->current ) ) {
712
+ return;
713
+ }
714
+
715
+ $this->options->job->current = $table;
716
+ $this->options->job->start = 0;
717
+ }
718
+
719
+ /**
720
+ * Start Job
721
+ * @param string $new
722
+ * @param string $old
723
+ * @return bool
724
+ */
725
+ private function startJob( $new, $old ) {
726
+
727
+ if( $this->isExcludedTable( $new ) ) {
728
+ return false;
729
+ }
730
+
731
+ // Table does not exist
732
+ $result = $this->productionDb->query( "SHOW TABLES LIKE '{$old}'" );
733
+ if( !$result || 0 === $result ) {
734
+ return false;
735
+ }
736
+
737
+ if( 0 != $this->options->job->start ) {
738
+ return true;
739
+ }
740
+
741
+ $this->options->job->total = ( int ) $this->productionDb->get_var( "SELECT COUNT(1) FROM {$old}" );
742
+
743
+ if( 0 == $this->options->job->total ) {
744
+ $this->finishStep();
745
+ return false;
746
+ }
747
+
748
+ return true;
749
+ }
750
+
751
+ /**
752
+ * Is table excluded from search replace processing?
753
+ * @param string $table
754
+ * @return boolean
755
+ */
756
+ private function isExcludedTable( $table ) {
757
+
758
+ $customTables = apply_filters( 'wpstg_clone_searchreplace_tables_exclude', array() );
759
+ $defaultTables = array('blogs');
760
+
761
+ $tables = array_merge( $customTables, $defaultTables );
762
+
763
+ $excludedTables = array();
764
+ foreach ( $tables as $key => $value ) {
765
+ $excludedTables[] = $this->options->prefix . $value;
766
+ }
767
+
768
+ if( in_array( $table, $excludedTables ) ) {
769
+ return true;
770
+ }
771
+ return false;
772
+ }
773
+
774
+ /**
775
+ * Finish the step
776
+ */
777
+ private function finishStep() {
778
+ // This job is not finished yet
779
+ if( $this->options->job->total > $this->options->job->start ) {
780
+ return false;
781
+ }
782
+
783
+ // Add it to cloned tables listing
784
+ $this->options->clonedTables[] = $this->options->tables[$this->options->currentStep];
785
+
786
+ // Reset job
787
+ $this->options->job = new \stdClass();
788
+
789
+ return true;
790
+ }
791
+
792
+ /**
793
+ * Drop table if necessary
794
+ * @param string $new
795
+ */
796
+ private function dropTable( $new ) {
797
+ $old = $this->stagingDb->get_var( $this->stagingDb->prepare( "SHOW TABLES LIKE %s", $new ) );
798
+
799
+ if( !$this->shouldDropTable( $new, $old ) ) {
800
+ return;
801
+ }
802
+
803
+ $this->log( "DB Processing: {$new} already exists, dropping it first" );
804
+ $this->stagingDb->query( "DROP TABLE {$new}" );
805
+ }
806
+
807
+ /**
808
+ * Check if table needs to be dropped
809
+ * @param string $new
810
+ * @param string $old
811
+ * @return bool
812
+ */
813
+ private function shouldDropTable( $new, $old ) {
814
+ return (
815
+ $old == $new &&
816
+ (
817
+ !isset( $this->options->job->current ) ||
818
+ !isset( $this->options->job->start ) ||
819
+ 0 == $this->options->job->start
820
+ )
821
+ );
822
+ }
823
+
824
+ /**
825
+ * Check if WP is installed in subdir
826
+ * @return boolean
827
+ */
828
+ private function isSubDir() {
829
+ // Compare names without scheme to bypass cases where siteurl and home have different schemes http / https
830
+ // This is happening much more often than you would expect
831
+ $siteurl = preg_replace( '#^https?://#', '', rtrim( get_option( 'siteurl' ), '/' ) );
832
+ $home = preg_replace( '#^https?://#', '', rtrim( get_option( 'home' ), '/' ) );
833
+
834
+ if( $home !== $siteurl ) {
835
+ return true;
836
+ }
837
+ return false;
838
+ }
839
+
840
+ }
apps/Backend/Modules/Jobs/SearchReplace.php CHANGED
@@ -339,7 +339,7 @@ class SearchReplace extends JobExecutable {
339
 
340
  //Make sure value is never smaller than 1 or greater than 20000
341
  //$end = $this->settings->querySRLimit == '0' || empty( $this->settings->querySRLimit ) ? 1 : $this->settings->querySRLimit > 20000 ? 20000 : $this->settings->querySRLimit;
342
- $end = $this->settings->querySRLimit;
343
 
344
  // Grab the content of the current table.
345
  $data = $this->db->get_results( "SELECT * FROM $table LIMIT $start, $end", ARRAY_A );
@@ -368,7 +368,6 @@ class SearchReplace extends JobExecutable {
368
  // $this->settings->querySRLimit = $end;
369
  // update_option( 'wpstg_settings', $this->settings );
370
  // }
371
-
372
  // Filter certain rows (of other plugins)
373
  $filter = array(
374
  'Admin_custome_login_Slidshow',
@@ -510,8 +509,6 @@ class SearchReplace extends JobExecutable {
510
  return true;
511
  }
512
 
513
-
514
-
515
  /**
516
  * Adapted from interconnect/it's search/replace script.
517
  *
@@ -531,6 +528,10 @@ class SearchReplace extends JobExecutable {
531
  */
532
  private function recursive_unserialize_replace( $from = '', $to = '', $data = '', $serialized = false, $case_insensitive = false ) {
533
  try {
 
 
 
 
534
  // Some unserialized data cannot be re-serialized eg. SimpleXMLElements
535
  if( is_serialized( $data ) && ( $unserialized = @unserialize( $data ) ) !== false ) {
536
  $data = $this->recursive_unserialize_replace( $from, $to, $unserialized, true, $case_insensitive );
@@ -554,7 +555,7 @@ class SearchReplace extends JobExecutable {
554
  }
555
  $tmp->$key = $this->recursive_unserialize_replace( $from, $to, $value, false, $case_insensitive );
556
  }
557
- $data = $tmp;
558
  $tmp = '';
559
  $props = '';
560
  unset( $tmp );
339
 
340
  //Make sure value is never smaller than 1 or greater than 20000
341
  //$end = $this->settings->querySRLimit == '0' || empty( $this->settings->querySRLimit ) ? 1 : $this->settings->querySRLimit > 20000 ? 20000 : $this->settings->querySRLimit;
342
+ $end = $this->settings->querySRLimit;
343
 
344
  // Grab the content of the current table.
345
  $data = $this->db->get_results( "SELECT * FROM $table LIMIT $start, $end", ARRAY_A );
368
  // $this->settings->querySRLimit = $end;
369
  // update_option( 'wpstg_settings', $this->settings );
370
  // }
 
371
  // Filter certain rows (of other plugins)
372
  $filter = array(
373
  'Admin_custome_login_Slidshow',
509
  return true;
510
  }
511
 
 
 
512
  /**
513
  * Adapted from interconnect/it's search/replace script.
514
  *
528
  */
529
  private function recursive_unserialize_replace( $from = '', $to = '', $data = '', $serialized = false, $case_insensitive = false ) {
530
  try {
531
+ // PDO instances can not be serialized or unserialized
532
+ if( is_serialized( $data ) && strpos( $data, 'O:3:"PDO":0:' ) !== false ) {
533
+ return $data;
534
+ }
535
  // Some unserialized data cannot be re-serialized eg. SimpleXMLElements
536
  if( is_serialized( $data ) && ( $unserialized = @unserialize( $data ) ) !== false ) {
537
  $data = $this->recursive_unserialize_replace( $from, $to, $unserialized, true, $case_insensitive );
555
  }
556
  $tmp->$key = $this->recursive_unserialize_replace( $from, $to, $value, false, $case_insensitive );
557
  }
558
+ $data = $tmp;
559
  $tmp = '';
560
  $props = '';
561
  unset( $tmp );
apps/Backend/helpers/wp-config.php CHANGED
@@ -1,57 +1,57 @@
1
- <?php
2
- /**
3
- * The base configuration for WordPress
4
- *
5
- * The wp-config.php creation script uses this file during the
6
- * installation. You don't have to use the web site, you can
7
- * copy this file to "wp-config.php" and fill in the values.
8
- *
9
- * This file contains the following configurations:
10
- *
11
- * * MySQL settings
12
- * * Secret keys
13
- * * Database table prefix
14
- * * ABSPATH
15
- *
16
- * @link https://codex.wordpress.org/Editing_wp-config.php
17
- *
18
- * @package WordPress
19
- */
20
-
21
- // ** MySQL settings ** //
22
-
23
- /**
24
- * Authentication Unique Keys and Salts.
25
- *
26
- * Change these to different unique phrases!
27
- * You can generate these using the {@link https://api.wordpress.org/secret-key/1.1/salt/ WordPress.org secret-key service}
28
- * You can change these at any point in time to invalidate all existing cookies. This will force all users to have to log in again.
29
- *
30
- * @since 2.6.0
31
- */
32
- define( 'AUTH_KEY', 'cwC1]Y4Qz<-jS?V W0U*CbW~$yY9U@=-t5X{][-S0GF~LqY yt[ChoYm@?`}iJzm' );
33
- define( 'SECURE_AUTH_KEY', 'dmZ5$.d:gCbUxX)FZ+[h-t81O>Yd5W!x<:D[q;6{A?;Q0F>fvKcjaM V0V?-XD(t' );
34
- define( 'LOGGED_IN_KEY', '{B%(DDM,))zFtDD8gLk;N^EK`iiG<V`XNZ/k~d]ne^t/MLN85o5ILrMWC!.:cq.X' );
35
- define( 'NONCE_KEY', 'tvt!~WL>)x{ ``SK6ZO^/R1lwZPerR?&(W>]h/da(Z^M$2?)ZDVsICxQV3?/h6)U' );
36
- define( 'AUTH_SALT', ';%e$?CJm$s-N!a;(B;NM/>_~gDuPa(VM1t:nUvQ+LZw;e]1)_`-qwCZe,-@^{Xd%' );
37
- define( 'SECURE_AUTH_SALT', '? nh*~x!7Jm^4E4w(y(qmaPK:$l5aB6!n6L}$IN+cXZSsE?<2~FfK|qN=s:=P+(c' );
38
- define( 'LOGGED_IN_SALT', '=Ty=Q):}E/[pW3y4IDN@Bas/&-MInTxFXziE)9H9^rnl g7TUj-7OP*UX2Oyz=Y$' );
39
- define( 'NONCE_SALT', 'HATY?A^EQ#F;oN8!W-oe5P%)aFaeU,E;rLFmRm&u-<g6tL9k(pyh77_,Kc _q2BY' );
40
- define( 'WP_CACHE_KEY_SALT', '5Y@>B@S8O{a w%ASH BX!;wu/RBk.HT[~R{csF.r)5f0q/YTy%$8lwV4o0eygz};' );
41
-
42
- /**
43
- * WordPress Database Table prefix.
44
- *
45
- * You can have multiple installations in one database if you give each
46
- * a unique prefix. Only numbers, letters, and underscores please!
47
- */
48
- $table_prefix = 'wp_';
49
-
50
- /* That's all, stop editing! Happy blogging. */
51
-
52
- /** Absolute path to the WordPress directory. */
53
- if ( ! defined( 'ABSPATH' ) )
54
- define( 'ABSPATH', dirname( __FILE__ ) . '/' );
55
-
56
- /** Sets up WordPress vars and included files. */
57
  require_once ABSPATH . 'wp-settings.php';
1
+ <?php
2
+ /**
3
+ * The base configuration for WordPress
4
+ *
5
+ * The wp-config.php creation script uses this file during the
6
+ * installation. You don't have to use the web site, you can
7
+ * copy this file to "wp-config.php" and fill in the values.
8
+ *
9
+ * This file contains the following configurations:
10
+ *
11
+ * * MySQL settings
12
+ * * Secret keys
13
+ * * Database table prefix
14
+ * * ABSPATH
15
+ *
16
+ * @link https://codex.wordpress.org/Editing_wp-config.php
17
+ *
18
+ * @package WordPress
19
+ */
20
+
21
+ // ** MySQL settings ** //
22
+
23
+ /**
24
+ * Authentication Unique Keys and Salts.
25
+ *
26
+ * Change these to different unique phrases!
27
+ * You can generate these using the {@link https://api.wordpress.org/secret-key/1.1/salt/ WordPress.org secret-key service}
28
+ * You can change these at any point in time to invalidate all existing cookies. This will force all users to have to log in again.
29
+ *
30
+ * @since 2.6.0
31
+ */
32
+ define( 'AUTH_KEY', 'cwC1]Y4Qz<-jS?V W0U*CbW~$yY9U@=-t5X{][-S0GF~LqY yt[ChoYm@?`}iJzm' );
33
+ define( 'SECURE_AUTH_KEY', 'dmZ5$.d:gCbUxX)FZ+[h-t81O>Yd5W!x<:D[q;6{A?;Q0F>fvKcjaM V0V?-XD(t' );
34
+ define( 'LOGGED_IN_KEY', '{B%(DDM,))zFtDD8gLk;N^EK`iiG<V`XNZ/k~d]ne^t/MLN85o5ILrMWC!.:cq.X' );
35
+ define( 'NONCE_KEY', 'tvt!~WL>)x{ ``SK6ZO^/R1lwZPerR?&(W>]h/da(Z^M$2?)ZDVsICxQV3?/h6)U' );
36
+ define( 'AUTH_SALT', ';%e$?CJm$s-N!a;(B;NM/>_~gDuPa(VM1t:nUvQ+LZw;e]1)_`-qwCZe,-@^{Xd%' );
37
+ define( 'SECURE_AUTH_SALT', '? nh*~x!7Jm^4E4w(y(qmaPK:$l5aB6!n6L}$IN+cXZSsE?<2~FfK|qN=s:=P+(c' );
38
+ define( 'LOGGED_IN_SALT', '=Ty=Q):}E/[pW3y4IDN@Bas/&-MInTxFXziE)9H9^rnl g7TUj-7OP*UX2Oyz=Y$' );
39
+ define( 'NONCE_SALT', 'HATY?A^EQ#F;oN8!W-oe5P%)aFaeU,E;rLFmRm&u-<g6tL9k(pyh77_,Kc _q2BY' );
40
+ define( 'WP_CACHE_KEY_SALT', '5Y@>B@S8O{a w%ASH BX!;wu/RBk.HT[~R{csF.r)5f0q/YTy%$8lwV4o0eygz};' );
41
+
42
+ /**
43
+ * WordPress Database Table prefix.
44
+ *
45
+ * You can have multiple installations in one database if you give each
46
+ * a unique prefix. Only numbers, letters, and underscores please!
47
+ */
48
+ $table_prefix = 'wp_';
49
+
50
+ /* That's all, stop editing! Happy blogging. */
51
+
52
+ /** Absolute path to the WordPress directory. */
53
+ if ( ! defined( 'ABSPATH' ) )
54
+ define( 'ABSPATH', dirname( __FILE__ ) . '/' );
55
+
56
+ /** Sets up WordPress vars and included files. */
57
  require_once ABSPATH . 'wp-settings.php';
apps/Backend/public/css/wpstg-admin-feedback.css ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ PLUGINS ADMIN PAGE
3
+ */
4
+ #wpstg-feedback-overlay {
5
+ /* Height & width depends on how you want to reveal the overlay (see JS below) */
6
+ height: 100%;
7
+ width: 100%;
8
+ position: fixed; /* Stay in place */
9
+ z-index: 10000; /* Sit on top */
10
+ left: 0;
11
+ top: 0;
12
+ background-color: rgb(120,120,120); /* Black fallback color */
13
+ background-color: rgba(0,0,0, 0.5); /* Black w/opacity */
14
+ }
15
+ #wpstg-feedback-content {
16
+ position: relative;
17
+ top: 25%; /* 25% from the top */
18
+ width: 500px;
19
+ max-width: 100%;
20
+ margin: auto;
21
+ margin-top: 30px; /* 30px top margin to avoid conflict with the close button on smaller screens */
22
+ max-height: 50%;
23
+ padding: 20px;
24
+ background-color: #fff;
25
+ overflow-y: auto;
26
+ }
27
+ #wpstg-feedback-content textarea,
28
+ #wpstg-feedback-content input[type="text"] { display:none;width:100%; }
29
+ .wpstg-feedback-not-deactivate { display: block; text-align: right; }
30
+
31
+ #wpstg-feedback-content h3{
32
+ margin:5px;
33
+ }
34
+
35
+ @media screen and (max-width:400px){
36
+ #wpstg-feedback-content {
37
+ padding:0px;
38
+ padding-bottom:50px;
39
+ }
40
+ }
apps/Backend/public/js/wpstg-admin-plugins.js ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var strict;
2
+
3
+ jQuery(document).ready(function ($) {
4
+
5
+ /**
6
+ * DEACTIVATION FEEDBACK FORM
7
+ */
8
+ // show overlay when clicked on "deactivate"
9
+ wpstg_deactivate_link = $('.wp-admin.plugins-php tr[data-slug="wp-staging"] .row-actions .deactivate a');
10
+ wpstg_deactivate_link_url = wpstg_deactivate_link.attr('href');
11
+
12
+ wpstg_deactivate_link.click(function (e) {
13
+ e.preventDefault();
14
+
15
+ // only show feedback form once per 30 days
16
+ var c_value = wpstg_admin_get_cookie("wpstg_hide_feedback");
17
+
18
+ if (c_value === undefined) {
19
+ $('#wpstg-feedback-overlay').show();
20
+ } else {
21
+ // click on the link
22
+ window.location.href = wpstg_deactivate_link_url;
23
+ }
24
+ });
25
+ // show text fields
26
+ $('#wpstg-feedback-content input[type="radio"]').click(function () {
27
+ // show text field if there is one
28
+ $(this).parents('li').next('li').children('input[type="text"], textarea').show();
29
+ });
30
+ // send form or close it
31
+ $('#wpstg-feedback-content .button').click(function (e) {
32
+ e.preventDefault();
33
+ // set cookie for 30 days
34
+ var exdate = new Date();
35
+ exdate.setSeconds(exdate.getSeconds() + 2592000);
36
+ document.cookie = "wpstg_hide_feedback=1; expires=" + exdate.toUTCString() + "; path=/";
37
+
38
+ $('#wpstg-feedback-overlay').hide();
39
+ if ('wpstg-feedback-submit' === this.id) {
40
+ // Send form data
41
+ $.ajax({
42
+ type: 'POST',
43
+ url: ajaxurl,
44
+ dataType: 'json',
45
+ data: {
46
+ action: 'wpstg_send_feedback',
47
+ data: $('#wpstg-feedback-content form').serialize()
48
+ },
49
+ complete: function (MLHttpRequest, textStatus, errorThrown) {
50
+ // deactivate the plugin and close the popup
51
+ $('#wpstg-feedback-overlay').remove();
52
+ window.location.href = wpstg_deactivate_link_url;
53
+
54
+ }
55
+ });
56
+ } else {
57
+ $('#wpstg-feedback-overlay').remove();
58
+ window.location.href = wpstg_deactivate_link_url;
59
+ }
60
+ });
61
+ // close form without doing anything
62
+ $('.wpstg-feedback-not-deactivate').click(function (e) {
63
+ $('#wpstg-feedback-overlay').hide();
64
+ });
65
+
66
+ function wpstg_admin_get_cookie (name) {
67
+ var i, x, y, wpstg_cookies = document.cookie.split( ";" );
68
+ for (i = 0; i < wpstg_cookies.length; i++)
69
+ {
70
+ x = wpstg_cookies[i].substr( 0, wpstg_cookies[i].indexOf( "=" ) );
71
+ y = wpstg_cookies[i].substr( wpstg_cookies[i].indexOf( "=" ) + 1 );
72
+ x = x.replace( /^\s+|\s+$/g, "" );
73
+ if (x === name)
74
+ {
75
+ return unescape( y );
76
+ }
77
+ }
78
+ }
79
+
80
+ });
apps/Backend/views/{clone/includes → _main}/footer.php RENAMED
@@ -7,7 +7,7 @@
7
  - <a href="https://wp-staging.com/docs/staging-site-redirects-live-site/" target="_blank" rel="external">Can not login to staging site</a> <br/>
8
  - <a href="https://wp-staging.com/docs/fix-white-or-blank-page-after-pushing-fatal-error-500/" target="_blank" rel="external">Staging site returns blank white page</a> <br/>
9
  - <a href="https://wp-staging.com/docs/activate-permalinks-staging-site/" target="_blank" rel="external">Activate permalinks on staging site</a> <br/>
10
- - <a href="https://wp-staging.com/docs/install-wp-staging/" target="_blank" rel="external">More</a><br/><br/>
11
  <?php echo __('It still does not work?','wp-staging');?>
12
  <br>
13
  <?php echo sprintf (__('Open a <a href="%s" target="_blank" rel="external nofollow">support ticket</a> and we will resolve it quickly.', 'wp-staging'), 'https://wp-staging.com/support');?>
7
  - <a href="https://wp-staging.com/docs/staging-site-redirects-live-site/" target="_blank" rel="external">Can not login to staging site</a> <br/>
8
  - <a href="https://wp-staging.com/docs/fix-white-or-blank-page-after-pushing-fatal-error-500/" target="_blank" rel="external">Staging site returns blank white page</a> <br/>
9
  - <a href="https://wp-staging.com/docs/activate-permalinks-staging-site/" target="_blank" rel="external">Activate permalinks on staging site</a> <br/>
10
+ - <a href="https://wp-staging.com/docs/troubleshooting-try-this-first/" target="_blank" rel="external">More</a><br/><br/>
11
  <?php echo __('It still does not work?','wp-staging');?>
12
  <br>
13
  <?php echo sprintf (__('Open a <a href="%s" target="_blank" rel="external nofollow">support ticket</a> and we will resolve it quickly.', 'wp-staging'), 'https://wp-staging.com/support');?>
apps/Backend/views/{_includes → _main}/header.php RENAMED
File without changes
apps/Backend/views/{_includes → _main}/report-issue.php RENAMED
File without changes
apps/Backend/views/clone/index.php CHANGED
@@ -1,8 +1,8 @@
1
  <div id="wpstg-clonepage-wrapper">
2
 
3
  <?php
4
- require_once($this->path . "views/_includes/header.php");
5
- require_once($this->path . "views/_includes/report-issue.php");
6
  ?>
7
 
8
  <?php
@@ -20,6 +20,6 @@
20
  }
21
 
22
  // Footer
23
- require_once($this->path . "views/clone/includes/footer.php");
24
  ?>
25
  </div>
1
  <div id="wpstg-clonepage-wrapper">
2
 
3
  <?php
4
+ require_once($this->path . "views/_main/header.php");
5
+ require_once($this->path . "views/_main/report-issue.php");
6
  ?>
7
 
8
  <?php
20
  }
21
 
22
  // Footer
23
+ require_once($this->path . "views/_main/footer.php");
24
  ?>
25
  </div>
apps/Backend/views/clone/staging-site/index.php CHANGED
@@ -1,4 +1,4 @@
1
- <span class="wpstg-notice-alert" style="margin-top:20px;">
2
- <?php echo sprintf(__("This is your staging site. Go to your live site to use that function.", "wp-staging"),
3
- admin_url() . 'admin.php?page=wpstg_clone')?>
4
  </span>
1
+ <span class="wpstg-notice-alert" style="margin-top:20px;">
2
+ <?php echo sprintf(__("This is your staging site. Go to your live site to use that function.", "wp-staging"),
3
+ admin_url() . 'admin.php?page=wpstg_clone')?>
4
  </span>
apps/Backend/views/feedback/deactivate-feedback.php ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ $reasons = array(
3
+ 1 => '<li><label><input type="radio" name="wpstg_disable_reason" value="temporary"/>' . __('Only temporary', 'wp-staging') . '</label></li>',
4
+ //2 => '<li><label><input type="radio" name="wpstg_disable_reason" value="stopped showing social buttons"/>' . __('I do not use it any longer ', 'wp-staging') . '</label></li>',
5
+ 3 => '<li><label><input type="radio" name="wpstg_disable_reason" value="missing feature"/>' . __('Miss a feature', 'wp-staging') . '</label></li>
6
+ <li><input type="text" name="wpstg_disable_text[]" value="" placeholder="Please describe the feature"/></li>',
7
+ 4 => '<li><label><input type="radio" name="wpstg_disable_reason" value="technical issue"/>' . __('Technical Issue', 'wp-staging') . '</label></li>
8
+ <li><textarea name="wpstg_disable_text[]" placeholder="' . __('Can we help? Please describe your problem', 'wp-staging') . '"></textarea></li>',
9
+ 5 => '<li><label><input type="radio" name="wpstg_disable_reason" value="other plugin"/>' . __('Switched to another plugin/staging solution', 'wp-staging') . '</label></li>
10
+ <li><input type="text" name="wpstg_disable_text[]" value="" placeholder="Name of the plugin"/></li>',
11
+ 6 => '<li><label><input type="radio" name="wpstg_disable_reason" value="other"/>' . __('Other reason', 'wp-staging') . '</label></li>
12
+ <li><textarea name="wpstg_disable_text[]" placeholder="' . __('Please specify, if possible', 'wp-staging') . '"></textarea></li>',
13
+ );
14
+ shuffle($reasons);
15
+ ?>
16
+
17
+
18
+ <div id="wpstg-feedback-overlay" style="display: none;">
19
+ <div id="wpstg-feedback-content">
20
+ <form action="" method="post">
21
+ <h3><strong><?php _e('Please let us know why you are deactivating:', 'wp-staging'); ?></strong></h3>
22
+ <ul>
23
+ <?php
24
+ foreach ($reasons as $reason){
25
+ echo $reason;
26
+ }
27
+ ?>
28
+ </ul>
29
+ <?php if ($email) : ?>
30
+ <input type="hidden" name="wpstg_disable_from" value="<?php echo $email; ?>"/>
31
+ <?php endif; ?>
32
+ <input id="wpstg-feedback-submit" class="button button-primary" type="submit" name="wpstg_disable_submit" value="<?php _e('Submit & Deactivate', 'wp-staging'); ?>"/>
33
+ <a class="button"><?php _e('Only Deactivate', 'wp-staging'); ?></a>
34
+ <a class="wpstg-feedback-not-deactivate" href="#"><?php _e('Don\'t deactivate', 'wp-staging'); ?></a>
35
+ </form>
36
+ </div>
37
+ </div>
apps/Backend/views/welcome/welcome.php CHANGED
@@ -1,7 +1,7 @@
1
  <div class="" id="wpstg-welcome">
2
  <div style="border: 2px solid white;padding: 20px;margin-bottom:20px;">
3
  <h2 class="wpstg-h2">
4
- <span class="wpstg-heading-pro"><?php _e( 'WP Staging Pro', 'wp-staging' ); ?></span><?php _e( ' - Copy Themes & Plugins from Staging to Live Site', 'wp-staging' ); ?>
5
  </h2>
6
  <li><strong>Cloning</strong> - <?php _e( 'Create a clone of your website with a simple click', 'wp-staging' ); ?></li>
7
  <li><strong>Push Changes</strong> - <?php _e( 'Copy plugin and theme files from staging to live site', 'wp-staging' ); ?></li>
1
  <div class="" id="wpstg-welcome">
2
  <div style="border: 2px solid white;padding: 20px;margin-bottom:20px;">
3
  <h2 class="wpstg-h2">
4
+ <span class="wpstg-heading-pro"><?php _e( 'WP Staging Pro', 'wp-staging' ); ?></span><?php _e( ' - Migrate and copy over staging site to live site', 'wp-staging' ); ?>
5
  </h2>
6
  <li><strong>Cloning</strong> - <?php _e( 'Create a clone of your website with a simple click', 'wp-staging' ); ?></li>
7
  <li><strong>Push Changes</strong> - <?php _e( 'Copy plugin and theme files from staging to live site', 'wp-staging' ); ?></li>
apps/Core/WPStaging.php CHANGED
@@ -29,7 +29,7 @@ final class WPStaging {
29
  /**
30
  * Plugin version
31
  */
32
- const VERSION = "2.5.9";
33
 
34
  /**
35
  * Plugin name
@@ -46,8 +46,6 @@ final class WPStaging {
46
  */
47
  const WP_COMPATIBLE = "5.2.2";
48
 
49
- //public $wpPath;
50
-
51
  /**
52
  * Slug: Either wp-staging or wp-staging-pro
53
  * @var string
@@ -160,6 +158,16 @@ final class WPStaging {
160
  $interval = new Cron();
161
  }
162
 
 
 
 
 
 
 
 
 
 
 
163
  /**
164
  * Scripts and Styles
165
  * @param string $hook
@@ -171,6 +179,16 @@ final class WPStaging {
171
  wp_enqueue_style( "wpstg-admin-bar", $this->backend_url . "css/wpstg-admin-bar.css", array(), $this->getVersion() );
172
  }
173
 
 
 
 
 
 
 
 
 
 
 
174
  $availablePages = array(
175
  "toplevel_page_wpstg_clone",
176
  "wp-staging_page_wpstg-settings",
@@ -190,6 +208,8 @@ final class WPStaging {
190
  "wpstg-admin-script", $this->backend_url . "js/wpstg-admin.js", array("jquery"), $this->getVersion(), false
191
  );
192
 
 
 
193
  // Load admin css files
194
  wp_enqueue_style(
195
  "wpstg-admin", $this->backend_url . "css/wpstg-admin.css", array(), $this->getVersion()
29
  /**
30
  * Plugin version
31
  */
32
+ const VERSION = "2.6.1";
33
 
34
  /**
35
  * Plugin name
46
  */
47
  const WP_COMPATIBLE = "5.2.2";
48
 
 
 
49
  /**
50
  * Slug: Either wp-staging or wp-staging-pro
51
  * @var string
158
  $interval = new Cron();
159
  }
160
 
161
+ /**
162
+ * Check if current page is plugins.php
163
+ * @global array $pagenow
164
+ * @return bool
165
+ */
166
+ private function isPluginsPage() {
167
+ global $pagenow;
168
+ return ( 'plugins.php' === $pagenow );
169
+ }
170
+
171
  /**
172
  * Scripts and Styles
173
  * @param string $hook
179
  wp_enqueue_style( "wpstg-admin-bar", $this->backend_url . "css/wpstg-admin-bar.css", array(), $this->getVersion() );
180
  }
181
 
182
+ // Load js file on page plugins.php
183
+ if( $this->isPluginsPage() ) {
184
+ wp_enqueue_script(
185
+ "wpstg-admin-script", $this->backend_url . "js/wpstg-admin-plugins.js", array("jquery"), $this->getVersion(), false
186
+ );
187
+ wp_enqueue_style(
188
+ "wpstg-admin-feedback", $this->backend_url . "css/wpstg-admin-feedback.css", array(), $this->getVersion()
189
+ );
190
+ }
191
+
192
  $availablePages = array(
193
  "toplevel_page_wpstg_clone",
194
  "wp-staging_page_wpstg-settings",
208
  "wpstg-admin-script", $this->backend_url . "js/wpstg-admin.js", array("jquery"), $this->getVersion(), false
209
  );
210
 
211
+
212
+
213
  // Load admin css files
214
  wp_enqueue_style(
215
  "wpstg-admin", $this->backend_url . "css/wpstg-admin.css", array(), $this->getVersion()
apps/Frontend/loginForm.php CHANGED
@@ -51,7 +51,7 @@ class loginForm {
51
  do_action( 'wp_login', $_POST['wpstg-username'], get_userdata( $user_data->ID ) );
52
 
53
  $redirect_to = get_site_url() . '/wp-admin/';
54
-
55
  if( !empty( $_POST['redirect_to'] ) ) {
56
  $redirectTo = $_POST['redirect_to'];
57
  }
@@ -79,144 +79,148 @@ class loginForm {
79
  <meta name="viewport" content="width=device-width">
80
  <meta name='robots' content='noindex,follow' />
81
  <title>WordPress &rsaquo; You need to login to access that page</title>
 
82
  <style type="text/css">
 
 
 
 
 
 
83
  html {
84
  background: #f1f1f1;
85
  }
 
86
  body {
87
- background: #fff;
88
  color: #444;
89
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
90
- margin: 2em auto;
91
- padding: 1em 2em;
92
- max-width: 700px;
93
- -webkit-box-shadow: 0 1px 3px rgba(0,0,0,0.13);
94
- box-shadow: 0 1px 3px rgba(0,0,0,0.13);
95
  }
96
- h1 {
97
- border-bottom: 1px solid #dadada;
98
- clear: both;
99
- color: #666;
100
- font-size: 24px;
101
- margin: 30px 0 0 0;
102
- padding: 0;
103
- padding-bottom: 7px;
104
  }
105
- #error-page {
106
- margin-top: 50px;
 
 
 
 
 
 
 
 
107
  }
108
- #error-page p {
 
 
 
 
 
109
  font-size: 14px;
110
- line-height: 1.5;
111
- margin: 25px 0 20px;
112
- }
113
- #error-page code {
114
- font-family: Consolas, Monaco, monospace;
115
  }
116
- ul li {
117
- margin-bottom: 10px;
118
- font-size: 14px ;
 
 
119
  }
120
- a {
121
- color: #0073aa;
 
122
  }
123
- a:hover,
124
- a:active {
125
- color: #00a0d2;
 
126
  }
127
- a:focus {
128
- color: #124964;
129
- -webkit-box-shadow:
130
- 0 0 0 1px #5b9dd9,
131
- 0 0 2px 1px rgba(30, 140, 190, .8);
132
- box-shadow:
133
- 0 0 0 1px #5b9dd9,
134
- 0 0 2px 1px rgba(30, 140, 190, .8);
135
- outline: none;
136
  }
137
- .button {
 
138
  background: #f7f7f7;
139
  border: 1px solid #ccc;
140
  color: #555;
141
  display: inline-block;
142
  text-decoration: none;
143
- font-size: 13px;
144
- line-height: 26px;
145
- height: 28px;
146
  margin: 0;
147
- padding: 0 10px 1px;
148
  cursor: pointer;
149
  -webkit-border-radius: 3px;
150
  -webkit-appearance: none;
151
  border-radius: 3px;
152
  white-space: nowrap;
153
- -webkit-box-sizing: border-box;
154
- -moz-box-sizing: border-box;
155
- box-sizing: border-box;
156
-
157
- -webkit-box-shadow: 0 1px 0 #ccc;
158
- box-shadow: 0 1px 0 #ccc;
159
  vertical-align: top;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  }
161
 
162
- .button.button-large {
163
- height: 30px;
164
- line-height: 28px;
165
- padding: 0 12px 2px;
166
  }
167
 
168
- .button:hover,
169
- .button:focus {
170
- background: #fafafa;
171
- border-color: #999;
172
- color: #23282d;
173
  }
174
 
175
- .button:focus {
176
- border-color: #5b9dd9;
177
- -webkit-box-shadow: 0 0 3px rgba( 0, 115, 170, .8 );
178
- box-shadow: 0 0 3px rgba( 0, 115, 170, .8 );
179
- outline: none;
 
 
 
180
  }
181
 
182
- .button:active {
183
- background: #eee;
184
- border-color: #999;
185
- -webkit-box-shadow: inset 0 2px 5px -3px rgba( 0, 0, 0, 0.5 );
186
- box-shadow: inset 0 2px 5px -3px rgba( 0, 0, 0, 0.5 );
187
- -webkit-transform: translateY(1px);
188
- -ms-transform: translateY(1px);
189
- transform: translateY(1px);
 
190
  }
191
 
 
 
 
 
 
 
 
 
 
 
192
  </style>
193
  </head>
194
  <?php
195
  }
196
 
197
  /**
198
- * Render login form by using native wp function wp_login_form
199
- * return string
200
  */
201
- // private function getLoginForm() {
202
- // $this->getHeader();
203
- // echo '<body id="error-page">';
204
- // echo __( 'Access denied. Login to access this site', 'wp-staging' );
205
- //
206
- // $args = array(
207
- // 'redirect' => admin_url(),
208
- // 'redirect' => admin_url(),
209
- // 'form_id' => 'wpstg-loginform',
210
- // 'label_username' => __( 'Username', 'wp-staging' ),
211
- // 'label_password' => __( 'Password', 'wp-staging' ),
212
- // 'label_remember' => __( 'Remember Me' ),
213
- // 'label_log_in' => __( 'Log In Staging Site' ),
214
- // 'remember' => true
215
- // );
216
- // wp_login_form( $args );
217
- // $this->getFooter();
218
- // }
219
-
220
  private function getFooter() {
221
  echo '</html>';
222
  }
@@ -259,7 +263,7 @@ class loginForm {
259
  // Default 'redirect' value takes the user back to the request URI.
260
  'redirect' => ( is_ssl() ? 'https://' : 'http://' ) . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'],
261
  'form_id' => 'loginform',
262
- 'label_username' => __( 'Username or Email Address' ),
263
  'label_password' => __( 'Password' ),
264
  'label_remember' => __( 'Remember Me' ),
265
  'label_log_in' => __( 'Log In' ),
@@ -276,26 +280,31 @@ class loginForm {
276
  $args = empty( $this->args ) ? $arguments : $this->args;
277
 
278
  $form = '
279
- <form name="' . $args['form_id'] . '" id="' . $args['form_id'] . '" action="" method="post">
280
- <p class="login-username">
281
- <label for="' . esc_attr( $args['id_username'] ) . '">' . esc_html( $args['label_username'] ) . '</label>
282
- <input type="text" name="wpstg-username" id="' . esc_attr( $args['id_username'] ) . '" class="input" value="' . esc_attr( $args['value_username'] ) . '" size="20" />
283
- </p>
284
- <p class="login-password">
285
- <label for="' . esc_attr( $args['id_password'] ) . '">' . esc_html( $args['label_password'] ) . '</label>
286
- <input type="password" name="wpstg-pass" id="' . esc_attr( $args['id_password'] ) . '" class="input" value="" size="20" />
287
- </p>
288
- ' . ( $args['remember'] ? '<p class="login-remember"><label><input name="rememberme" type="checkbox" id="' . esc_attr( $args['id_remember'] ) . '" value="forever"' . ( $args['value_remember'] ? ' checked="checked"' : '' ) . ' /> ' . esc_html( $args['label_remember'] ) . '</label></p>' : '' ) . '
289
- <p class="login-submit">
290
- <input type="submit" name="wpstg-submit" id="' . esc_attr( $args['id_submit'] ) . '" class="button button-primary" value="' . esc_attr( $args['label_log_in'] ) . '" />
291
- <input type="hidden" name="redirect_to" value="' . esc_url( $args['redirect'] ) . '" />
292
- </p>
293
- <p>
294
- ' . $this->error . '
295
- </p>
296
-
297
-
298
- </form>';
 
 
 
 
 
299
 
300
  if( $args['echo'] )
301
  echo $form;
51
  do_action( 'wp_login', $_POST['wpstg-username'], get_userdata( $user_data->ID ) );
52
 
53
  $redirect_to = get_site_url() . '/wp-admin/';
54
+
55
  if( !empty( $_POST['redirect_to'] ) ) {
56
  $redirectTo = $_POST['redirect_to'];
57
  }
79
  <meta name="viewport" content="width=device-width">
80
  <meta name='robots' content='noindex,follow' />
81
  <title>WordPress &rsaquo; You need to login to access that page</title>
82
+
83
  <style type="text/css">
84
+
85
+ * {
86
+ -webkit-box-sizing: border-box;
87
+ box-sizing: border-box;
88
+ }
89
+
90
  html {
91
  background: #f1f1f1;
92
  }
93
+
94
  body {
 
95
  color: #444;
96
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
97
+ margin: 0;
 
 
 
 
98
  }
99
+
100
+ .wp-staging-login {
101
+ padding: 1rem;
 
 
 
 
 
102
  }
103
+
104
+ .wp-staging-form {
105
+ -webkit-box-shadow: 0 1px 3px rgba(0,0,0,0.13);
106
+ box-shadow: 0 1px 3px rgba(0,0,0,0.13);
107
+ max-width: 500px;
108
+ width: 100%;
109
+ margin: 3rem auto;
110
+ background: #fff;
111
+ padding: 1rem;
112
+ overflow: hidden;
113
  }
114
+
115
+ .form-control {
116
+ width: 100%;
117
+ border: 1px solid #ced4da;
118
+ border-radius: .25rem;
119
+ padding: 0.75rem 1rem;
120
  font-size: 14px;
 
 
 
 
 
121
  }
122
+
123
+ .form-control:focus {
124
+ outline: 0;
125
+ -webkit-box-shadow: 0 0 0 0.1rem rgba(221,221,221,.35);
126
+ box-shadow: 0 0 0 0.1rem rgba(221,221,221,.35);
127
  }
128
+
129
+ .form-group {
130
+ margin: 0 0 1em;
131
  }
132
+
133
+ .form-group label {
134
+ margin: 0 0 0.5em;
135
+ display: block;
136
  }
137
+
138
+ .login-remember input {
139
+ margin-top: 0;
140
+ vertical-align: middle;
 
 
 
 
 
141
  }
142
+
143
+ .btn {
144
  background: #f7f7f7;
145
  border: 1px solid #ccc;
146
  color: #555;
147
  display: inline-block;
148
  text-decoration: none;
149
+ font-size: 14px;
 
 
150
  margin: 0;
151
+ padding: 0.65rem 1.1rem;
152
  cursor: pointer;
153
  -webkit-border-radius: 3px;
154
  -webkit-appearance: none;
155
  border-radius: 3px;
156
  white-space: nowrap;
 
 
 
 
 
 
157
  vertical-align: top;
158
+ -webkit-transition: -webkit-box-shadow 0.2s ease;
159
+ transition: -webkit-box-shadow 0.2s ease;
160
+ -o-transition: box-shadow 0.2s ease;
161
+ transition: box-shadow 0.2s ease;
162
+ transition: box-shadow 0.2s ease, -webkit-box-shadow 0.2s ease;
163
+ }
164
+
165
+ .btn:hover {
166
+ -webkit-box-shadow: 0 0 0 0.1rem rgba(221,221,221,.35);
167
+ box-shadow: 0 0 0 0.1rem rgba(221,221,221,.35);
168
+ }
169
+
170
+ #error-page {
171
+ margin-top: 50px;
172
  }
173
 
174
+ #error-page p {
175
+ font-size: 14px;
176
+ line-height: 1.5;
177
+ margin: 25px 0 20px;
178
  }
179
 
180
+ #error-page code {
181
+ font-family: Consolas, Monaco, monospace;
 
 
 
182
  }
183
 
184
+ .error-msg {
185
+ -webkit-animation: slideIn 0.3s ease;
186
+ animation: slideIn 0.3s ease;
187
+ color: #ff4c4c;
188
+ }
189
+
190
+ .password-lost{
191
+ padding-top:20px;
192
  }
193
 
194
+ @-webkit-keyframes slideIn {
195
+ 0% {
196
+ -webkit-transform: translateX(-100%);
197
+ transform: translateX(-100%);
198
+ }
199
+ 100% {
200
+ -webkit-transform: translateX(0);
201
+ transform: translateX(0);
202
+ }
203
  }
204
 
205
+ @keyframes slideIn {
206
+ 0% {
207
+ -webkit-transform: translateX(-100%);
208
+ transform: translateX(-100%);
209
+ }
210
+ 100% {
211
+ -webkit-transform: translateX(0);
212
+ transform: translateX(0);
213
+ }
214
+ }
215
  </style>
216
  </head>
217
  <?php
218
  }
219
 
220
  /**
221
+ * Add footer
222
+ *
223
  */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
  private function getFooter() {
225
  echo '</html>';
226
  }
263
  // Default 'redirect' value takes the user back to the request URI.
264
  'redirect' => ( is_ssl() ? 'https://' : 'http://' ) . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'],
265
  'form_id' => 'loginform',
266
+ 'label_username' => __( 'Username' ),
267
  'label_password' => __( 'Password' ),
268
  'label_remember' => __( 'Remember Me' ),
269
  'label_log_in' => __( 'Log In' ),
280
  $args = empty( $this->args ) ? $arguments : $this->args;
281
 
282
  $form = '
283
+ <main class="wp-staging-login" >
284
+ <form class="wp-staging-form" name="' . $args['form_id'] . '" id="' . $args['form_id'] . '" action="" method="post">
285
+ <div class="form-group login-username">
286
+ <label for="' . esc_attr( $args['id_username'] ) . '">' . esc_html( $args['label_username'] ) . '</label>
287
+ <input type="text" name="wpstg-username" id="' . esc_attr( $args['id_username'] ) . '" class="input form-control" value="' . esc_attr( $args['value_username'] ) . '" size="20" />
288
+ </div>
289
+ <div class=" form-group login-password">
290
+ <label for="' . esc_attr( $args['id_password'] ) . '">' . esc_html( $args['label_password'] ) . '</label>
291
+ <input type="password" name="wpstg-pass" id="' . esc_attr( $args['id_password'] ) . '" class="input form-control" value="" size="20" />
292
+ </div>
293
+ ' . ( $args['remember'] ? '
294
+ <div class="form-group login-remember"><label><input name="rememberme" type="checkbox" id="' . esc_attr( $args['id_remember'] ) . '" value="forever"' . ( $args['value_remember'] ? ' checked="checked"' : '' ) . ' /> <span>' . esc_html( $args['label_remember'] ) . '</span></label></div>
295
+ ' : '' ) . '
296
+ <div class="login-submit">
297
+ <button type="submit" name="wpstg-submit" id="' . esc_attr( $args['id_submit'] ) . '" class="btn" value="' . esc_attr( $args['label_log_in'] ) . '">Login</button>
298
+ <input type="hidden" name="redirect_to" value="' . esc_url( $args['redirect'] ) . '" />
299
+ </div>
300
+ <div class="password-lost">
301
+ <a href="' . trailingslashit(esc_url( $args['redirect'] )) . 'wp-login.php?action=lostpassword">Lost your password?</a>
302
+ </div>
303
+ <p class="error-msg">
304
+ ' . $this->error . '
305
+ </p>
306
+ </form>
307
+ </main>';
308
 
309
  if( $args['echo'] )
310
  echo $form;
languages/wp-staging-de_DE.po CHANGED
@@ -339,19 +339,19 @@ msgstr "Klon Bezeichnung"
339
  msgid "Attention: Check carefully if these database tables and files are safe to delete and do not belong to your live site!"
340
  msgstr "Achtung: Überprüfe sorgfältig ob diese Datenbank Tabellen und Dateien sicher gelöscht werden dürfen und nicht zu Deiner Live Seite gehören!"
341
 
342
- #: apps/Backend/views/_includes/report-issue.php:26
343
  msgid "Send Issue"
344
  msgstr "Abschicken"
345
 
346
- #: apps/Backend/views/_includes/report-issue.php:20
347
  msgid "By submitting, I accept the <a href=\"https://wp-staging.com/privacy-policy/\" target=\"_blank\">Privacy Policy</a> and consent that my email will be stored and processed for the purposes of proving support."
348
  msgstr "Mit Übertragen akzeptiere ich die <a href=\"https://wp-staging.com/privacy-policy/\" target=\"_blank\">Datenschutz Vereinbarung</a> und erlaube, dass meine E-Mail Adresse gespeichert und verarbeitet wird um meine Support Anfrage zu beantworten."
349
 
350
- #: apps/Backend/views/_includes/report-issue.php:12
351
  msgid "Optional: Submit the <a href=\"%s\" target=\"_blank\">System Log</a>. This helps us to resolve your technical issues."
352
  msgstr "Optional: Übertrage das <a href=\"%s\" target=\"_blank\">System Log</a>. Das hilft und Dein technisches Anliegen zu lösen."
353
 
354
- #: apps/Backend/views/_includes/messages/staging-directory-permission-problem.php:4
355
  msgid ""
356
  "WP Staging Folder Permission error:</strong>\n"
357
  " %1$s is not write and/or readable.\n"
@@ -365,19 +365,19 @@ msgstr ""
365
  " Überprüfe ob der Ordner <strong>%1$s</strong> beschreibbar ist vom PHP Benutzer www-data.\n"
366
  " File permissions should be chmod 755 or 777."
367
 
368
- #: apps/Backend/views/_includes/messages/rating.php:27
369
  msgid "No, not good enough"
370
  msgstr "Nein, nicht gut genug"
371
 
372
- #: apps/Backend/views/_includes/messages/rating.php:22
373
  msgid "I already did"
374
  msgstr "Habe ich schon getan"
375
 
376
- #: apps/Backend/views/_includes/messages/rating.php:17
377
  msgid "Ok, you deserved it"
378
  msgstr "Ok, hast Du verdient"
379
 
380
- #: apps/Backend/views/_includes/messages/rating.php:5
381
  msgid ""
382
  "P.S. Looking for a way to migrate the staging site database and copy plugins and theme files from staging to live site?<br/>\n"
383
  " Try out <a href=\"%1$s\" target=\"_blank\" style=\"color:white;font-weight:bold;\">WP Staging Pro</a>\n"
@@ -387,23 +387,23 @@ msgstr ""
387
  "Probiere <a href=\"%1$s\" target=\"_blank\" style=\"color:white;font-weight:bold;\">WP Staging Pro</a>\n"
388
  " aus"
389
 
390
- #: apps/Backend/views/_includes/messages/rating.php:2
391
  msgid ""
392
  " Awesome, you've been using <strong>WP Staging </strong> for more than 1 week.\n"
393
  " May I ask you to give it a <strong>5-star</strong> rating on Wordpress?"
394
  msgstr "Toll, Du verwendest <strong>WP Staging</strong> seit mehr als eine Woche. Darf ich Dich bitten eine 5-Sterne Bewertung auf WordPress.org abzugeben?"
395
 
396
- #: apps/Backend/views/_includes/header.php:28
397
  #: apps/Backend/views/settings/main-settings.php:29
398
  msgid "Share on Facebook"
399
  msgstr "Teile auf Facebook"
400
 
401
- #: apps/Backend/views/_includes/header.php:21
402
  #: apps/Backend/views/settings/main-settings.php:22
403
  msgid "Follow @wpstaging"
404
  msgstr "Folge @wpstaging"
405
 
406
- #: apps/Backend/views/_includes/header.php:14
407
  #: apps/Backend/views/settings/main-settings.php:15
408
  msgid "Tweet #wpstaging"
409
  msgstr "Tweet #wpstaging"
@@ -413,12 +413,12 @@ msgid "Start Now"
413
  msgstr "Jetzt Starten"
414
 
415
  #: apps/Backend/Notices/Notices.php:145
416
- #: apps/Backend/views/_includes/messages/transient.php:7
417
  msgid "WP Staging and WP Staging Pro cannot both be active. We've automatically deactivated WP Staging Pro."
418
  msgstr "WP Staging und WP Staging Pro können nicht beide gleichzeitig aktiv sein. Wir haben WP Staging Pro daher deaktiviert."
419
 
420
  #: apps/Backend/Notices/Notices.php:143
421
- #: apps/Backend/views/_includes/messages/transient.php:6
422
  msgid "WP Staging and WP Staging Pro cannot both be active. We've automatically deactivated WP Staging."
423
  msgstr "WP Staging und WP Staging Pro können nicht beide gleichzeitig aktiv sein. Wir haben WP Staging daher deaktiviert."
424
 
339
  msgid "Attention: Check carefully if these database tables and files are safe to delete and do not belong to your live site!"
340
  msgstr "Achtung: Überprüfe sorgfältig ob diese Datenbank Tabellen und Dateien sicher gelöscht werden dürfen und nicht zu Deiner Live Seite gehören!"
341
 
342
+ #: apps/Backend/views/_main/report-issue.php:26
343
  msgid "Send Issue"
344
  msgstr "Abschicken"
345
 
346
+ #: apps/Backend/views/_main/report-issue.php:20
347
  msgid "By submitting, I accept the <a href=\"https://wp-staging.com/privacy-policy/\" target=\"_blank\">Privacy Policy</a> and consent that my email will be stored and processed for the purposes of proving support."
348
  msgstr "Mit Übertragen akzeptiere ich die <a href=\"https://wp-staging.com/privacy-policy/\" target=\"_blank\">Datenschutz Vereinbarung</a> und erlaube, dass meine E-Mail Adresse gespeichert und verarbeitet wird um meine Support Anfrage zu beantworten."
349
 
350
+ #: apps/Backend/views/_main/report-issue.php:12
351
  msgid "Optional: Submit the <a href=\"%s\" target=\"_blank\">System Log</a>. This helps us to resolve your technical issues."
352
  msgstr "Optional: Übertrage das <a href=\"%s\" target=\"_blank\">System Log</a>. Das hilft und Dein technisches Anliegen zu lösen."
353
 
354
+ #: apps/Backend/views/_main/messages/staging-directory-permission-problem.php:4
355
  msgid ""
356
  "WP Staging Folder Permission error:</strong>\n"
357
  " %1$s is not write and/or readable.\n"
365
  " Überprüfe ob der Ordner <strong>%1$s</strong> beschreibbar ist vom PHP Benutzer www-data.\n"
366
  " File permissions should be chmod 755 or 777."
367
 
368
+ #: apps/Backend/views/_main/messages/rating.php:27
369
  msgid "No, not good enough"
370
  msgstr "Nein, nicht gut genug"
371
 
372
+ #: apps/Backend/views/_main/messages/rating.php:22
373
  msgid "I already did"
374
  msgstr "Habe ich schon getan"
375
 
376
+ #: apps/Backend/views/_main/messages/rating.php:17
377
  msgid "Ok, you deserved it"
378
  msgstr "Ok, hast Du verdient"
379
 
380
+ #: apps/Backend/views/_main/messages/rating.php:5
381
  msgid ""
382
  "P.S. Looking for a way to migrate the staging site database and copy plugins and theme files from staging to live site?<br/>\n"
383
  " Try out <a href=\"%1$s\" target=\"_blank\" style=\"color:white;font-weight:bold;\">WP Staging Pro</a>\n"
387
  "Probiere <a href=\"%1$s\" target=\"_blank\" style=\"color:white;font-weight:bold;\">WP Staging Pro</a>\n"
388
  " aus"
389
 
390
+ #: apps/Backend/views/_main/messages/rating.php:2
391
  msgid ""
392
  " Awesome, you've been using <strong>WP Staging </strong> for more than 1 week.\n"
393
  " May I ask you to give it a <strong>5-star</strong> rating on Wordpress?"
394
  msgstr "Toll, Du verwendest <strong>WP Staging</strong> seit mehr als eine Woche. Darf ich Dich bitten eine 5-Sterne Bewertung auf WordPress.org abzugeben?"
395
 
396
+ #: apps/Backend/views/_main/header.php:28
397
  #: apps/Backend/views/settings/main-settings.php:29
398
  msgid "Share on Facebook"
399
  msgstr "Teile auf Facebook"
400
 
401
+ #: apps/Backend/views/_main/header.php:21
402
  #: apps/Backend/views/settings/main-settings.php:22
403
  msgid "Follow @wpstaging"
404
  msgstr "Folge @wpstaging"
405
 
406
+ #: apps/Backend/views/_main/header.php:14
407
  #: apps/Backend/views/settings/main-settings.php:15
408
  msgid "Tweet #wpstaging"
409
  msgstr "Tweet #wpstaging"
413
  msgstr "Jetzt Starten"
414
 
415
  #: apps/Backend/Notices/Notices.php:145
416
+ #: apps/Backend/views/_main/messages/transient.php:7
417
  msgid "WP Staging and WP Staging Pro cannot both be active. We've automatically deactivated WP Staging Pro."
418
  msgstr "WP Staging und WP Staging Pro können nicht beide gleichzeitig aktiv sein. Wir haben WP Staging Pro daher deaktiviert."
419
 
420
  #: apps/Backend/Notices/Notices.php:143
421
+ #: apps/Backend/views/_main/messages/transient.php:6
422
  msgid "WP Staging and WP Staging Pro cannot both be active. We've automatically deactivated WP Staging."
423
  msgstr "WP Staging und WP Staging Pro können nicht beide gleichzeitig aktiv sein. Wir haben WP Staging daher deaktiviert."
424
 
readme.txt CHANGED
@@ -9,7 +9,7 @@ License URI: http://www.gnu.org/licenses/gpl-2.0.html
9
  Tags: staging, duplication, cloning, clone, migration, sandbox, test site, testing, backup, post, admin, administration, duplicate posts
10
  Requires at least: 3.6+
11
  Tested up to: 5.2
12
- Stable tag: 2.5.9
13
  Requires PHP: 5.3
14
 
15
  A duplicator plugin! Clone, duplicate and migrate live sites to independent staging and development sites that are available only to administrators.
@@ -151,6 +151,15 @@ https://wp-staging.com
151
 
152
  == Changelog ==
153
 
 
 
 
 
 
 
 
 
 
154
  = 2.6.0 =
155
  * New: Compatible up to WordPress 5.2.2
156
  * New: Performance improvement for directory iterator using less server ressources
@@ -158,8 +167,6 @@ https://wp-staging.com
158
  * Fix: Error conditions in class Data does not compare type strict (== vs. ==) resulting in interruption of clone process
159
  * Fix: Excluded folders under wp-content level are not take into account on microsoft IIS servers
160
 
161
-
162
-
163
  = 2.5.9 =
164
  * New: Update for WP 5.2.1
165
  * New: Better corporate identity and more friendly UI colors for staging sites listings and button
@@ -244,11 +251,3 @@ https://wp-staging.com
244
 
245
 
246
  Complete changelog: [https://wp-staging.com/wp-staging-changelog](https://wp-staging.com/wp-staging-changelog)
247
-
248
- == Upgrade Notice ==
249
-
250
- = 2.5.9 =
251
- * New: Increase the cloning/pushing speed by benchmarking and automatic tuning the maximum possible performance settings
252
- * New: Better warning notices before updating process is executed
253
- * Fix: Prevent wordfence firewall rule interupting the clone deletion method
254
- * Fix: Excluded wp staging directory from deleting process is ignored and will be deleted either way
9
  Tags: staging, duplication, cloning, clone, migration, sandbox, test site, testing, backup, post, admin, administration, duplicate posts
10
  Requires at least: 3.6+
11
  Tested up to: 5.2
12
+ Stable tag: 2.6.1
13
  Requires PHP: 5.3
14
 
15
  A duplicator plugin! Clone, duplicate and migrate live sites to independent staging and development sites that are available only to administrators.
151
 
152
  == Changelog ==
153
 
154
+ = 2.6.1 =
155
+ * New: Improve styling of login form. Thanks to Andy Kennan (Screaming Frog)
156
+ * New: Add 'password lost' button to login form
157
+ * New: Change welcome page CTA
158
+ * New: Add feedback option when plugin is disabled
159
+ * Fix: PDO instances can not be serialized or unserialized
160
+ * Fix: Can not update staging site db table if there are constraints in it
161
+
162
+
163
  = 2.6.0 =
164
  * New: Compatible up to WordPress 5.2.2
165
  * New: Performance improvement for directory iterator using less server ressources
167
  * Fix: Error conditions in class Data does not compare type strict (== vs. ==) resulting in interruption of clone process
168
  * Fix: Excluded folders under wp-content level are not take into account on microsoft IIS servers
169
 
 
 
170
  = 2.5.9 =
171
  * New: Update for WP 5.2.1
172
  * New: Better corporate identity and more friendly UI colors for staging sites listings and button
251
 
252
 
253
  Complete changelog: [https://wp-staging.com/wp-staging-changelog](https://wp-staging.com/wp-staging-changelog)
 
 
 
 
 
 
 
 
wp-staging.php CHANGED
@@ -7,7 +7,7 @@
7
  * Author: WP-Staging
8
  * Author URI: https://wp-staging.com
9
  * Contributors: ReneHermi, ilgityildirim
10
- * Version: 2.5.9
11
  * Text Domain: wp-staging
12
  * Domain Path: /languages/
13
 
@@ -51,7 +51,7 @@ if( !defined( 'WPSTG_PLUGIN_URL' ) ) {
51
 
52
  // Version
53
  if( !defined( 'WPSTG_VERSION' ) ) {
54
- define( 'WPSTG_VERSION', '2.5.9' );
55
  }
56
 
57
  // Must use version of the optimizer
7
  * Author: WP-Staging
8
  * Author URI: https://wp-staging.com
9
  * Contributors: ReneHermi, ilgityildirim
10
+ * Version: 2.6.1
11
  * Text Domain: wp-staging
12
  * Domain Path: /languages/
13
 
51
 
52
  // Version
53
  if( !defined( 'WPSTG_VERSION' ) ) {
54
+ define( 'WPSTG_VERSION', '2.6.1' );
55
  }
56
 
57
  // Must use version of the optimizer