Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
40.60% |
54 / 133 |
|
50.00% |
6 / 12 |
CRAP | |
0.00% |
0 / 1 |
| Backup_Import_Manager | |
40.91% |
54 / 132 |
|
50.00% |
6 / 12 |
828.75 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
| import | |
26.98% |
17 / 63 |
|
0.00% |
0 / 1 |
380.34 | |||
| update_status | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
| bump_import_stats | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
30 | |||
| determine_importer_type | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
| get_importer | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
12 | |||
| should_bail_out | |
54.55% |
6 / 11 |
|
0.00% |
0 / 1 |
7.35 | |||
| reset_import_status | |
46.15% |
6 / 13 |
|
0.00% |
0 / 1 |
6.50 | |||
| delete_backup_import_status | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| is_import_cancelled | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
4 | |||
| get_backup_import_status | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| force_cache_unset | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * Backup_Import_Manager file. |
| 4 | * |
| 5 | * @package wpcomsh |
| 6 | */ |
| 7 | |
| 8 | namespace Imports; |
| 9 | |
| 10 | // Include the file extractor. |
| 11 | require_once __DIR__ . '/utils/class-fileextractor.php'; |
| 12 | |
| 13 | use WP_Error; |
| 14 | |
| 15 | /** |
| 16 | * Class Backup_Import_Manager |
| 17 | * |
| 18 | * This class is responsible for managing the import of backups. |
| 19 | */ |
| 20 | class Backup_Import_Manager { |
| 21 | /** |
| 22 | * The path to the ZIP or TAR file to be imported. |
| 23 | * |
| 24 | * @var string |
| 25 | */ |
| 26 | protected $zip_or_tar_file_path; |
| 27 | /** |
| 28 | * The path where the backup will be imported. |
| 29 | * |
| 30 | * @var string |
| 31 | */ |
| 32 | protected $destination_path; |
| 33 | /** |
| 34 | * An array of actions that the importer needs to perform. |
| 35 | * |
| 36 | * @var array |
| 37 | */ |
| 38 | protected $importer_actions = array( |
| 39 | 'preprocess', |
| 40 | 'process_files', |
| 41 | 'recreate_database', |
| 42 | 'postprocess_database', |
| 43 | 'verify_site_integrity', |
| 44 | 'clean_up', |
| 45 | ); |
| 46 | /** |
| 47 | * An array of options. |
| 48 | * |
| 49 | * @var array |
| 50 | */ |
| 51 | protected $options = array(); |
| 52 | |
| 53 | /** |
| 54 | * An array of valid option keys. |
| 55 | * |
| 56 | * @var array |
| 57 | */ |
| 58 | protected $valid_option_keys = array( |
| 59 | 'actions', |
| 60 | 'bump_stats', |
| 61 | 'dry_run', |
| 62 | 'skip_clean_up', |
| 63 | 'skip_unpack', |
| 64 | ); |
| 65 | |
| 66 | /** |
| 67 | * Importer type. |
| 68 | * |
| 69 | * @var string |
| 70 | */ |
| 71 | protected $importer_type = null; |
| 72 | |
| 73 | /** |
| 74 | * Constant representing the WordPress Playground importer type. |
| 75 | */ |
| 76 | const WORDPRESS_PLAYGROUND = 'wordpress_playground'; |
| 77 | /** |
| 78 | * Constant representing the Jetpack Backup importer type. |
| 79 | */ |
| 80 | const JETPACK_BACKUP = 'jetpack_backup'; |
| 81 | /** |
| 82 | * The prefix to use for temporary databases. |
| 83 | */ |
| 84 | const TEMPORARY_DB_PREFIX = 'tmp_'; |
| 85 | /** |
| 86 | * Constant representing the success status. |
| 87 | */ |
| 88 | const SUCCESS = 'success'; |
| 89 | /** |
| 90 | * Constant representing the failed status. |
| 91 | */ |
| 92 | const FAILED = 'failed'; |
| 93 | /** |
| 94 | * Constant representing the cancelled status. |
| 95 | */ |
| 96 | const CANCELLED = 'cancelled'; |
| 97 | |
| 98 | /** |
| 99 | * Backup import status option name. |
| 100 | * |
| 101 | * @var string |
| 102 | */ |
| 103 | public static $backup_import_status_option = 'backup_import_status'; |
| 104 | |
| 105 | /** |
| 106 | * Constructor for the Backup_Import_Manager class. |
| 107 | * |
| 108 | * This method initializes the $zip_or_tar_file_path and $destination_path properties. |
| 109 | * |
| 110 | * @param string $zip_or_tar_file_path The path to the ZIP or TAR file to be imported. |
| 111 | * @param string $destination_path The path where the backup will be imported. |
| 112 | * @param array $options An array of options. |
| 113 | */ |
| 114 | public function __construct( $zip_or_tar_file_path, $destination_path, $options = array() ) { |
| 115 | $this->zip_or_tar_file_path = $zip_or_tar_file_path; |
| 116 | $this->destination_path = trailingslashit( $destination_path ); |
| 117 | $this->options = array_intersect_key( $options, array_flip( $this->valid_option_keys ) ); |
| 118 | } |
| 119 | /** |
| 120 | * Import the backup. |
| 121 | * |
| 122 | * This method performs the following steps: |
| 123 | * 1. Extract the ZIP or TAR file to the destination path. |
| 124 | * 2. Determine the type of the importer based on the destination path. |
| 125 | * 3. Get an instance of the appropriate importer based on the type. |
| 126 | * 4. Call the importer's methods in the order specified in the $importer_actions array. |
| 127 | * |
| 128 | * @return bool|WP_Error True on success, or a WP_Error on failure. |
| 129 | */ |
| 130 | public function import() { |
| 131 | $skip_clean_up = false; |
| 132 | if ( isset( $this->options['skip_clean_up'] ) && is_bool( $this->options['skip_clean_up'] ) ) { |
| 133 | $skip_clean_up = $this->options['skip_clean_up']; |
| 134 | } |
| 135 | |
| 136 | $skip_unpack = false; |
| 137 | if ( isset( $this->options['skip_unpack'] ) && is_bool( $this->options['skip_unpack'] ) ) { |
| 138 | $skip_unpack = $this->options['skip_unpack']; |
| 139 | } |
| 140 | |
| 141 | $bump_stats = true; |
| 142 | if ( isset( $this->options['bump_stats'] ) && is_bool( $this->options['bump_stats'] ) ) { |
| 143 | $bump_stats = $this->options['bump_stats']; |
| 144 | } |
| 145 | |
| 146 | // check if there are import process that's already running |
| 147 | $check_bail_result = $this->should_bail_out(); |
| 148 | |
| 149 | if ( is_wp_error( $check_bail_result ) ) { |
| 150 | |
| 151 | // We don't update status to failed here, because we don't want to overwrite the status |
| 152 | |
| 153 | if ( $bump_stats ) { |
| 154 | $this->bump_import_stats( $check_bail_result->get_error_code() ); |
| 155 | } |
| 156 | |
| 157 | return $check_bail_result; |
| 158 | } |
| 159 | |
| 160 | // reset the import status before everything starts |
| 161 | self::delete_backup_import_status(); |
| 162 | |
| 163 | // unzip/untar the file |
| 164 | if ( ! $skip_unpack ) { |
| 165 | $this->update_status( array( 'status' => 'unpack_file' ) ); |
| 166 | $result = Utils\FileExtractor::extract( $this->zip_or_tar_file_path, $this->destination_path ); |
| 167 | |
| 168 | if ( is_wp_error( $result ) ) { |
| 169 | $this->update_status( array( 'status' => self::FAILED ) ); |
| 170 | |
| 171 | if ( $bump_stats ) { |
| 172 | $this->bump_import_stats( $result->get_error_code() ); |
| 173 | } |
| 174 | |
| 175 | return $result; |
| 176 | } |
| 177 | } |
| 178 | |
| 179 | // validate the type of the file |
| 180 | $importer_type = self::determine_importer_type( $this->destination_path ); |
| 181 | if ( is_wp_error( $importer_type ) ) { |
| 182 | $this->update_status( array( 'status' => self::FAILED ) ); |
| 183 | |
| 184 | if ( $bump_stats ) { |
| 185 | $this->bump_import_stats( $importer_type->get_error_code() ); |
| 186 | } |
| 187 | |
| 188 | return $importer_type; |
| 189 | } |
| 190 | |
| 191 | // get the importer |
| 192 | $importer = self::get_importer( $importer_type, $this->zip_or_tar_file_path, $this->destination_path ); |
| 193 | if ( is_wp_error( $importer ) ) { |
| 194 | $this->update_status( array( 'status' => self::FAILED ) ); |
| 195 | |
| 196 | if ( $bump_stats ) { |
| 197 | $this->bump_import_stats( $importer->get_error_code() ); |
| 198 | } |
| 199 | |
| 200 | return $importer; |
| 201 | } else { |
| 202 | $this->importer_type = $importer_type; |
| 203 | } |
| 204 | |
| 205 | $execute_actions = isset( $this->options['actions'] ) && count( $this->options['actions'] ) ? $this->options['actions'] : $this->importer_actions; |
| 206 | $dry_run = isset( $this->options['dry_run'] ) && $this->options['dry_run']; |
| 207 | |
| 208 | if ( $skip_clean_up ) { |
| 209 | foreach ( $execute_actions as $key => $action ) { |
| 210 | // Remove the cleanup action if the user has specified to skip cleanup. |
| 211 | if ( $action === 'clean_up' ) { |
| 212 | unset( $execute_actions[ $key ] ); |
| 213 | } |
| 214 | } |
| 215 | } |
| 216 | |
| 217 | foreach ( $execute_actions as $action ) { |
| 218 | if ( ! method_exists( $importer, $action ) ) { |
| 219 | continue; |
| 220 | } |
| 221 | |
| 222 | // Before calling the importer's method, let's check if the status is cancelled. |
| 223 | $cancel_result = $this->is_import_cancelled(); |
| 224 | |
| 225 | if ( true === $cancel_result ) { |
| 226 | // Clear the status. |
| 227 | self::delete_backup_import_status(); |
| 228 | |
| 229 | if ( $bump_stats ) { |
| 230 | $this->bump_import_stats( 'backup_import_cancelled' ); |
| 231 | } |
| 232 | |
| 233 | return new WP_Error( 'backup_import_cancelled', __( 'The backup import has been cancelled.', 'wpcomsh' ) ); |
| 234 | } |
| 235 | |
| 236 | $this->update_status( array( 'status' => $action ) ); |
| 237 | |
| 238 | if ( $dry_run ) { |
| 239 | // Wait for 15-20 seconds in dry-run mode. |
| 240 | sleep( \wp_rand( 15, 20 ) ); |
| 241 | } else { |
| 242 | // Call the importer's method. |
| 243 | $result = $importer->$action(); |
| 244 | |
| 245 | if ( is_wp_error( $result ) ) { |
| 246 | $this->update_status( array( 'status' => self::FAILED ) ); |
| 247 | |
| 248 | if ( $bump_stats ) { |
| 249 | $this->bump_import_stats( $result->get_error_code() ); |
| 250 | } |
| 251 | |
| 252 | return $result; |
| 253 | } |
| 254 | } |
| 255 | } |
| 256 | |
| 257 | if ( $bump_stats ) { |
| 258 | $this->bump_import_stats( 'success' ); |
| 259 | } |
| 260 | |
| 261 | return $this->update_status( array( 'status' => self::SUCCESS ) ); |
| 262 | } |
| 263 | |
| 264 | /** |
| 265 | * Updates the deployment status option. |
| 266 | * |
| 267 | * @param array $content The contents to be merged to the existing option. |
| 268 | * |
| 269 | * @return bool |
| 270 | */ |
| 271 | private function update_status( array $content ): bool { |
| 272 | $existing = \get_option( self::$backup_import_status_option, array() ); |
| 273 | $new = array_merge( $existing, $content ); |
| 274 | |
| 275 | \update_option( self::$backup_import_status_option, $new ); |
| 276 | self::force_cache_unset(); |
| 277 | |
| 278 | return true; |
| 279 | } |
| 280 | |
| 281 | /** |
| 282 | * Bump the import stats. |
| 283 | * |
| 284 | * @param string $status The status of the import. |
| 285 | * |
| 286 | * @return bool|WP_Error True on success, or a WP_Error on failure. |
| 287 | */ |
| 288 | private function bump_import_stats( string $status ) { |
| 289 | if ( isset( $this->options['dry_run'] ) && $this->options['dry_run'] ) { |
| 290 | return true; |
| 291 | } |
| 292 | |
| 293 | // Bumping at the same time the status and the type. |
| 294 | $query_args = array( |
| 295 | 'x_backup-import' => $status, |
| 296 | 'x_backup-import-type' => null === $this->importer_type ? 'unknown' : $this->importer_type, |
| 297 | 'v' => 'wpcom-no-pv', |
| 298 | ); |
| 299 | |
| 300 | $stats_track_url = 'http://pixel.wp.com/b.gif?' . http_build_query( $query_args ); |
| 301 | $result = wp_remote_get( $stats_track_url ); |
| 302 | |
| 303 | if ( is_wp_error( $result ) ) { |
| 304 | return $result; |
| 305 | } |
| 306 | |
| 307 | return true; |
| 308 | } |
| 309 | |
| 310 | /** |
| 311 | * Determine the type of the importer based on the file in destination path. |
| 312 | * |
| 313 | * @param string $destination_path The path where the backup will be imported. |
| 314 | * |
| 315 | * @return string|WP_Error The type of the importer or a WP_Error if the type could not be determined. |
| 316 | */ |
| 317 | public static function determine_importer_type( $destination_path ) { |
| 318 | if ( file_exists( $destination_path . 'wp-content/database/.ht.sqlite' ) ) { |
| 319 | return self::WORDPRESS_PLAYGROUND; |
| 320 | } |
| 321 | |
| 322 | if ( file_exists( $destination_path . 'wp-config.php' ) ) { |
| 323 | return self::JETPACK_BACKUP; |
| 324 | } |
| 325 | |
| 326 | return new WP_Error( 'unknown_importer_type', __( 'Could not determine importer type.', 'wpcomsh' ) ); |
| 327 | } |
| 328 | |
| 329 | /** |
| 330 | * Get an instance of the appropriate importer based on the type. |
| 331 | * |
| 332 | * @param string $type The type of the importer. |
| 333 | * @param string $zip_or_tar_file_path The path to the ZIP or TAR file to be imported. |
| 334 | * @param string $destination_path The path where the backup will be imported. |
| 335 | * |
| 336 | * @return Backup_Importer|WP_Error An instance of the appropriate importer or a WP_Error if the type is unknown. |
| 337 | */ |
| 338 | public static function get_importer( string $type, string $zip_or_tar_file_path, string $destination_path ) { |
| 339 | switch ( $type ) { |
| 340 | case self::WORDPRESS_PLAYGROUND: |
| 341 | require_once __DIR__ . '/playground/class-playground-importer.php'; |
| 342 | return new Playground_Importer( $zip_or_tar_file_path, $destination_path, self::TEMPORARY_DB_PREFIX ); |
| 343 | |
| 344 | default: |
| 345 | return new WP_Error( 'unknown_importer_type', __( 'Could not determine importer type.', 'wpcomsh' ) ); |
| 346 | } |
| 347 | } |
| 348 | |
| 349 | /** |
| 350 | * Checks if an import process is already running. |
| 351 | * |
| 352 | * @return false|WP_Error Returns WP_Error if an import process is running, false otherwise. |
| 353 | */ |
| 354 | private function should_bail_out() { |
| 355 | $additional_status_to_check = array( 'unpack_file' ); |
| 356 | $import_status = self::get_backup_import_status(); |
| 357 | $import_in_progress = false; |
| 358 | |
| 359 | if ( ! empty( $import_status ) ) { |
| 360 | // check if the status is one of other status |
| 361 | if ( in_array( $import_status['status'], $additional_status_to_check, true ) ) { |
| 362 | $import_in_progress = true; |
| 363 | } |
| 364 | // check if the status is one of the actions |
| 365 | if ( in_array( $import_status['status'], $this->importer_actions, true ) ) { |
| 366 | $import_in_progress = true; |
| 367 | } |
| 368 | } |
| 369 | |
| 370 | if ( $import_in_progress ) { |
| 371 | return new WP_Error( 'import_in_progress', __( 'An import is already running.', 'wpcomsh' ) ); |
| 372 | } |
| 373 | return false; |
| 374 | } |
| 375 | |
| 376 | /** |
| 377 | * Reset the import status. |
| 378 | * |
| 379 | * @return bool|WP_Error True on success, or a WP_Error on failure. |
| 380 | */ |
| 381 | public static function reset_import_status() { |
| 382 | $backup_import_status = self::get_backup_import_status(); |
| 383 | |
| 384 | if ( empty( $backup_import_status ) ) { |
| 385 | return new WP_Error( 'no_backup_import_found', __( 'No backup import found.', 'wpcomsh' ) ); |
| 386 | } |
| 387 | |
| 388 | if ( $backup_import_status['status'] === self::SUCCESS || $backup_import_status['status'] === self::FAILED ) { |
| 389 | // if it's a success or failed, we can delete the option directly |
| 390 | self::delete_backup_import_status(); |
| 391 | } else { |
| 392 | // Otherwise we set the status to cancelled and update the option |
| 393 | update_option( |
| 394 | self::$backup_import_status_option, |
| 395 | array( |
| 396 | 'status' => self::CANCELLED, |
| 397 | ), |
| 398 | ); |
| 399 | self::force_cache_unset(); |
| 400 | } |
| 401 | |
| 402 | return true; |
| 403 | } |
| 404 | |
| 405 | /** |
| 406 | * Deletes the backup import status option. |
| 407 | * |
| 408 | * @return void |
| 409 | */ |
| 410 | public static function delete_backup_import_status() { |
| 411 | delete_option( self::$backup_import_status_option ); |
| 412 | self::force_cache_unset(); |
| 413 | } |
| 414 | |
| 415 | /** |
| 416 | * Checks if the import process has been cancelled. |
| 417 | * |
| 418 | * @return mixed Returns WP_Error if the import has been cancelled, false otherwise. |
| 419 | */ |
| 420 | public function is_import_cancelled() { |
| 421 | |
| 422 | $backup_import_status = self::get_backup_import_status(); |
| 423 | |
| 424 | if ( empty( $backup_import_status ) ) { |
| 425 | // The import status doesn't exist, so we should stop here. |
| 426 | return new WP_Error( 'no_backup_import_found', __( 'No backup import found.', 'wpcomsh' ) ); |
| 427 | } |
| 428 | |
| 429 | if ( isset( $backup_import_status['status'] ) && $backup_import_status['status'] === self::CANCELLED ) { |
| 430 | // The import has been cancelled, so we should stop here. |
| 431 | return true; |
| 432 | } |
| 433 | |
| 434 | return false; |
| 435 | } |
| 436 | /** |
| 437 | * Get the backup import status. |
| 438 | * |
| 439 | * @return array|null Returns the backup import status or null if it doesn't exist. |
| 440 | */ |
| 441 | public static function get_backup_import_status() { |
| 442 | $backup_import_status = get_option( self::$backup_import_status_option, null ); |
| 443 | if ( is_array( $backup_import_status ) ) { |
| 444 | return $backup_import_status; |
| 445 | } |
| 446 | return null; |
| 447 | } |
| 448 | /** |
| 449 | * Force unset the cache for the backup import status option. |
| 450 | * |
| 451 | * @return void |
| 452 | */ |
| 453 | public static function force_cache_unset() { |
| 454 | $alloptions = wp_load_alloptions(); |
| 455 | if ( isset( $alloptions[ self::$backup_import_status_option ] ) ) { |
| 456 | unset( $alloptions[ self::$backup_import_status_option ] ); |
| 457 | wp_cache_set( 'alloptions', $alloptions, 'options' ); |
| 458 | } else { |
| 459 | wp_cache_delete( self::$backup_import_status_option, 'options' ); |
| 460 | } |
| 461 | } |
| 462 | } |