Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
53.14% |
93 / 175 |
|
50.00% |
10 / 20 |
CRAP | |
0.00% |
0 / 1 |
| Backup | |
53.18% |
92 / 173 |
|
50.00% |
10 / 20 |
353.30 | |
0.00% |
0 / 1 |
| register_endpoints | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
1 | |||
| get_name | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| get_title | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| get_description | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| get_long_description | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| get_features | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
| get_disclaimers | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
| get_wpcom_product_slug | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| get_post_checkout_url | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| get_pricing_for_ui | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
| permissions_callback | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| get_site_backup_undo_event | |
0.00% |
0 / 35 |
|
0.00% |
0 / 1 |
72 | |||
| get_state_from_wpcom | |
82.35% |
14 / 17 |
|
0.00% |
0 / 1 |
3.05 | |||
| get_latest_backups | |
82.35% |
14 / 17 |
|
0.00% |
0 / 1 |
3.05 | |||
| does_module_need_attention | |
28.21% |
11 / 39 |
|
0.00% |
0 / 1 |
137.90 | |||
| is_upgradable_by_bundle | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| get_post_activation_url | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| get_manage_url | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
| get_paid_plan_product_slugs | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
1 | |||
| get_status | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
5 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * Backup product |
| 4 | * |
| 5 | * @package my-jetpack |
| 6 | */ |
| 7 | |
| 8 | namespace Automattic\Jetpack\My_Jetpack\Products; |
| 9 | |
| 10 | use Automattic\Jetpack\Connection\Client; |
| 11 | use Automattic\Jetpack\My_Jetpack\Hybrid_Product; |
| 12 | use Automattic\Jetpack\My_Jetpack\Wpcom_Products; |
| 13 | use Automattic\Jetpack\Redirect; |
| 14 | use WP_Error; |
| 15 | |
| 16 | if ( ! defined( 'ABSPATH' ) ) { |
| 17 | exit( 0 ); |
| 18 | } |
| 19 | |
| 20 | /** |
| 21 | * Class responsible for handling the Backup product |
| 22 | */ |
| 23 | class Backup extends Hybrid_Product { |
| 24 | public const BACKUP_STATUS_TRANSIENT_KEY = 'my-jetpack-backup-status'; |
| 25 | |
| 26 | /** |
| 27 | * The product slug |
| 28 | * |
| 29 | * @var string |
| 30 | */ |
| 31 | public static $slug = 'backup'; |
| 32 | |
| 33 | /** |
| 34 | * The filename (id) of the plugin associated with this product. |
| 35 | * |
| 36 | * @var string |
| 37 | */ |
| 38 | public static $plugin_filename = array( |
| 39 | 'jetpack-backup/jetpack-backup.php', |
| 40 | 'backup/jetpack-backup.php', |
| 41 | 'jetpack-backup-dev/jetpack-backup.php', |
| 42 | ); |
| 43 | |
| 44 | /** |
| 45 | * The slug of the plugin associated with this product. |
| 46 | * |
| 47 | * @var string |
| 48 | */ |
| 49 | public static $plugin_slug = 'jetpack-backup'; |
| 50 | |
| 51 | /** |
| 52 | * The category of the product |
| 53 | * |
| 54 | * @var string |
| 55 | */ |
| 56 | public static $category = 'security'; |
| 57 | |
| 58 | /** |
| 59 | * Backup has a standalone plugin |
| 60 | * |
| 61 | * @var bool |
| 62 | */ |
| 63 | public static $has_standalone_plugin = true; |
| 64 | |
| 65 | /** |
| 66 | * Whether this product has a free offering |
| 67 | * |
| 68 | * @var bool |
| 69 | */ |
| 70 | public static $has_free_offering = false; |
| 71 | |
| 72 | /** |
| 73 | * Whether this product requires a plan to work at all |
| 74 | * |
| 75 | * @var bool |
| 76 | */ |
| 77 | public static $requires_plan = true; |
| 78 | |
| 79 | /** |
| 80 | * The feature slug that identifies the paid plan |
| 81 | * |
| 82 | * @var string |
| 83 | */ |
| 84 | public static $feature_identifying_paid_plan = 'backups'; |
| 85 | |
| 86 | /** |
| 87 | * Backup initialization |
| 88 | * |
| 89 | * @return void |
| 90 | */ |
| 91 | public static function register_endpoints(): void { |
| 92 | parent::register_endpoints(); |
| 93 | // Get backup undo event |
| 94 | register_rest_route( |
| 95 | 'my-jetpack/v1', |
| 96 | '/site/backup/undo-event', |
| 97 | array( |
| 98 | 'methods' => \WP_REST_Server::READABLE, |
| 99 | 'callback' => __CLASS__ . '::get_site_backup_undo_event', |
| 100 | 'permission_callback' => __CLASS__ . '::permissions_callback', |
| 101 | ) |
| 102 | ); |
| 103 | } |
| 104 | |
| 105 | /** |
| 106 | * Get the product name |
| 107 | * |
| 108 | * @return string |
| 109 | */ |
| 110 | public static function get_name() { |
| 111 | return 'VaultPress Backup'; |
| 112 | } |
| 113 | |
| 114 | /** |
| 115 | * Get the product title |
| 116 | * |
| 117 | * @return string |
| 118 | */ |
| 119 | public static function get_title() { |
| 120 | return 'Jetpack VaultPress Backup'; |
| 121 | } |
| 122 | |
| 123 | /** |
| 124 | * Get the internationalized product description |
| 125 | * |
| 126 | * @return string |
| 127 | */ |
| 128 | public static function get_description() { |
| 129 | return __( 'Real-time backups save every change, and one-click restores get you back online quickly.', 'jetpack-my-jetpack' ); |
| 130 | } |
| 131 | |
| 132 | /** |
| 133 | * Get the internationalized product long description |
| 134 | * |
| 135 | * @return string |
| 136 | */ |
| 137 | public static function get_long_description() { |
| 138 | return __( 'Never lose a word, image, page, or time worrying about your site with automated backups & one-click restores.', 'jetpack-my-jetpack' ); |
| 139 | } |
| 140 | |
| 141 | /** |
| 142 | * Get the internationalized features list |
| 143 | * |
| 144 | * @return array Backup features list |
| 145 | */ |
| 146 | public static function get_features() { |
| 147 | return array( |
| 148 | _x( 'Real-time cloud backups', 'Backup Product Feature', 'jetpack-my-jetpack' ), |
| 149 | _x( '10GB of backup storage', 'Backup Product Feature', 'jetpack-my-jetpack' ), |
| 150 | _x( '30-day archive & activity log*', 'Backup Product Feature', 'jetpack-my-jetpack' ), |
| 151 | _x( 'One-click restores', 'Backup Product Feature', 'jetpack-my-jetpack' ), |
| 152 | ); |
| 153 | } |
| 154 | |
| 155 | /** |
| 156 | * Get disclaimers corresponding to a feature |
| 157 | * |
| 158 | * @return array Backup disclaimers list |
| 159 | */ |
| 160 | public static function get_disclaimers() { |
| 161 | return array( |
| 162 | array( |
| 163 | 'text' => _x( '* Subject to your usage and storage limit.', 'Backup Product Disclaimer', 'jetpack-my-jetpack' ), |
| 164 | 'link_text' => _x( 'Learn more', 'Backup Product Disclaimer', 'jetpack-my-jetpack' ), |
| 165 | 'url' => Redirect::get_url( 'jetpack-faq-backup-disclaimer' ), |
| 166 | ), |
| 167 | ); |
| 168 | } |
| 169 | |
| 170 | /** |
| 171 | * Get the WPCOM product slug used to make the purchase |
| 172 | * |
| 173 | * @return ?string |
| 174 | */ |
| 175 | public static function get_wpcom_product_slug() { |
| 176 | return 'jetpack_backup_t1_yearly'; |
| 177 | } |
| 178 | |
| 179 | /** |
| 180 | * Get the URL where the user should be redirected after checkout |
| 181 | */ |
| 182 | public static function get_post_checkout_url() { |
| 183 | return self::get_manage_url(); |
| 184 | } |
| 185 | |
| 186 | /** |
| 187 | * Get the product princing details |
| 188 | * |
| 189 | * @return array Pricing details |
| 190 | */ |
| 191 | public static function get_pricing_for_ui() { |
| 192 | return array_merge( |
| 193 | array( |
| 194 | 'available' => true, |
| 195 | 'wpcom_product_slug' => static::get_wpcom_product_slug(), |
| 196 | ), |
| 197 | Wpcom_Products::get_product_pricing( static::get_wpcom_product_slug() ) |
| 198 | ); |
| 199 | } |
| 200 | |
| 201 | /** |
| 202 | * Checks if the user has the correct permissions |
| 203 | */ |
| 204 | public static function permissions_callback() { |
| 205 | return current_user_can( 'manage_options' ); |
| 206 | } |
| 207 | |
| 208 | /** |
| 209 | * This will fetch the last rewindable event from the Activity Log and |
| 210 | * the last rewind_id prior to that. |
| 211 | * |
| 212 | * @return array|WP_Error|null |
| 213 | */ |
| 214 | public static function get_site_backup_undo_event() { |
| 215 | $blog_id = \Jetpack_Options::get_option( 'id' ); |
| 216 | |
| 217 | $response = Client::wpcom_json_api_request_as_user( |
| 218 | '/sites/' . $blog_id . '/activity/rewindable?force=wpcom', |
| 219 | 'v2', |
| 220 | array(), |
| 221 | null, |
| 222 | 'wpcom' |
| 223 | ); |
| 224 | |
| 225 | if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { |
| 226 | return null; |
| 227 | } |
| 228 | |
| 229 | $body = json_decode( $response['body'], true ); |
| 230 | |
| 231 | if ( ! isset( $body['current'] ) ) { |
| 232 | return null; |
| 233 | } |
| 234 | |
| 235 | // Preparing the response structure |
| 236 | $undo_event = array( |
| 237 | 'last_rewindable_event' => null, |
| 238 | 'undo_backup_id' => null, |
| 239 | ); |
| 240 | |
| 241 | // List of events that will not be considered to be undo. |
| 242 | // Basically we should not `undo` a full backup event, but we could |
| 243 | // use them to undo any other action like plugin updates. |
| 244 | $last_event_exceptions = array( |
| 245 | 'rewind__backup_only_complete_full', |
| 246 | 'rewind__backup_only_complete_initial', |
| 247 | 'rewind__backup_only_complete', |
| 248 | 'rewind__backup_complete_full', |
| 249 | 'rewind__backup_complete_initial', |
| 250 | 'rewind__backup_complete', |
| 251 | ); |
| 252 | |
| 253 | // Looping through the events to find the last rewindable event and the last backup_id. |
| 254 | // The idea is to find the last rewindable event and then the last rewind_id before that. |
| 255 | $found_last_event = false; |
| 256 | foreach ( $body['current']['orderedItems'] as $event ) { |
| 257 | if ( $event['is_rewindable'] ) { |
| 258 | if ( ! $found_last_event && ! in_array( $event['name'], $last_event_exceptions, true ) ) { |
| 259 | $undo_event['last_rewindable_event'] = $event; |
| 260 | $found_last_event = true; |
| 261 | } elseif ( $found_last_event ) { |
| 262 | $undo_event['undo_backup_id'] = $event['rewind_id']; |
| 263 | break; |
| 264 | } |
| 265 | } |
| 266 | } |
| 267 | |
| 268 | return rest_ensure_response( $undo_event ); |
| 269 | } |
| 270 | |
| 271 | /** |
| 272 | * Hits the wpcom api to check rewind status. |
| 273 | * |
| 274 | * @todo Maybe add caching. |
| 275 | * |
| 276 | * @return object|WP_Error |
| 277 | */ |
| 278 | private static function get_state_from_wpcom() { |
| 279 | static $status = null; |
| 280 | |
| 281 | if ( $status !== null ) { |
| 282 | return $status; |
| 283 | } |
| 284 | |
| 285 | $site_id = \Jetpack_Options::get_option( 'id' ); |
| 286 | |
| 287 | $response = Client::wpcom_json_api_request_as_blog( |
| 288 | sprintf( '/sites/%d/rewind', $site_id ) . '?force=wpcom', |
| 289 | '2', |
| 290 | array( 'timeout' => 2 ), |
| 291 | null, |
| 292 | 'wpcom' |
| 293 | ); |
| 294 | |
| 295 | if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { |
| 296 | $status = new WP_Error( 'rewind_state_fetch_failed' ); |
| 297 | return $status; |
| 298 | } |
| 299 | |
| 300 | $body = wp_remote_retrieve_body( $response ); |
| 301 | $status = json_decode( $body ); |
| 302 | return $status; |
| 303 | } |
| 304 | |
| 305 | /** |
| 306 | * Hits the wpcom api to retrieve the last 10 backup records. |
| 307 | * |
| 308 | * @return object|WP_Error |
| 309 | */ |
| 310 | public static function get_latest_backups() { |
| 311 | static $backups = null; |
| 312 | |
| 313 | if ( $backups !== null ) { |
| 314 | return $backups; |
| 315 | } |
| 316 | |
| 317 | $site_id = \Jetpack_Options::get_option( 'id' ); |
| 318 | $response = Client::wpcom_json_api_request_as_blog( |
| 319 | sprintf( '/sites/%d/rewind/backups', $site_id ) . '?force=wpcom', |
| 320 | '2', |
| 321 | array( 'timeout' => 2 ), |
| 322 | null, |
| 323 | 'wpcom' |
| 324 | ); |
| 325 | |
| 326 | if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { |
| 327 | $backups = new WP_Error( 'rewind_backups_fetch_failed' ); |
| 328 | return $backups; |
| 329 | } |
| 330 | |
| 331 | $body = wp_remote_retrieve_body( $response ); |
| 332 | $backups = json_decode( $body ); |
| 333 | return $backups; |
| 334 | } |
| 335 | |
| 336 | /** |
| 337 | * Determines whether the module/plugin/product needs the users attention. |
| 338 | * Typically due to some sort of error where user troubleshooting is needed. |
| 339 | * |
| 340 | * @return boolean|array |
| 341 | */ |
| 342 | public static function does_module_need_attention() { |
| 343 | $previous_backup_status = get_transient( self::BACKUP_STATUS_TRANSIENT_KEY ); |
| 344 | |
| 345 | // If we have a previous backup status, show it. |
| 346 | if ( ! empty( $previous_backup_status ) ) { |
| 347 | return $previous_backup_status === 'no_errors' ? false : $previous_backup_status; |
| 348 | } |
| 349 | |
| 350 | $backup_failed_status = false; |
| 351 | // First check the status of Rewind for failure. |
| 352 | $rewind_state = self::get_state_from_wpcom(); |
| 353 | if ( ! is_wp_error( $rewind_state ) ) { |
| 354 | // Special case: 'unavailable' with 'site_new' reason is a normal provisioning state for brand new sites. |
| 355 | $is_new_site_provisioning = ( 'unavailable' === $rewind_state->state && |
| 356 | 'site_new' === ( $rewind_state->reason ?? '' ) ); |
| 357 | |
| 358 | if ( |
| 359 | ! in_array( $rewind_state->state, array( 'active', 'provisioning', 'awaiting_credentials' ), true ) && |
| 360 | ! $is_new_site_provisioning |
| 361 | ) { |
| 362 | $backup_failed_status = array( |
| 363 | 'type' => 'error', |
| 364 | 'data' => array( |
| 365 | 'source' => 'rewind', |
| 366 | 'status' => isset( $rewind_state->reason ) && ! empty( $rewind_state->reason ) ? $rewind_state->reason : $rewind_state->state, |
| 367 | 'last_updated' => $rewind_state->last_updated, |
| 368 | ), |
| 369 | ); |
| 370 | } |
| 371 | } |
| 372 | // Next check for a failed last backup. |
| 373 | $latest_backups = self::get_latest_backups(); |
| 374 | if ( ! is_wp_error( $latest_backups ) ) { |
| 375 | // Get the last/latest backup record. |
| 376 | $last_backup = null; |
| 377 | foreach ( $latest_backups as $backup ) { |
| 378 | if ( $backup->is_backup ) { |
| 379 | $last_backup = $backup; |
| 380 | break; |
| 381 | } |
| 382 | } |
| 383 | |
| 384 | if ( $last_backup && isset( $last_backup->status ) ) { |
| 385 | if ( $last_backup->status !== 'started' && ! preg_match( '/-will-retry$/', $last_backup->status ) && $last_backup->status !== 'finished' ) { |
| 386 | $backup_failed_status = array( |
| 387 | 'type' => 'error', |
| 388 | 'data' => array( |
| 389 | 'source' => 'last_backup', |
| 390 | 'status' => $last_backup->status, |
| 391 | 'last_updated' => $last_backup->last_updated, |
| 392 | ), |
| 393 | ); |
| 394 | } |
| 395 | } |
| 396 | } |
| 397 | |
| 398 | if ( is_array( $backup_failed_status ) ) { |
| 399 | set_transient( self::BACKUP_STATUS_TRANSIENT_KEY, $backup_failed_status, 5 * MINUTE_IN_SECONDS ); |
| 400 | } else { |
| 401 | set_transient( self::BACKUP_STATUS_TRANSIENT_KEY, 'no_errors', HOUR_IN_SECONDS ); |
| 402 | } |
| 403 | |
| 404 | return $backup_failed_status; |
| 405 | } |
| 406 | |
| 407 | /** |
| 408 | * Return product bundles list |
| 409 | * that supports the product. |
| 410 | * |
| 411 | * @return boolean|array Products bundle list. |
| 412 | */ |
| 413 | public static function is_upgradable_by_bundle() { |
| 414 | return array( 'security', 'complete' ); |
| 415 | } |
| 416 | |
| 417 | /** |
| 418 | * Get the URL the user is taken after activating the product |
| 419 | * |
| 420 | * @return ?string |
| 421 | */ |
| 422 | public static function get_post_activation_url() { |
| 423 | return ''; // stay in My Jetpack page or continue the purchase flow if needed. |
| 424 | } |
| 425 | |
| 426 | /** |
| 427 | * Get the URL where the user manages the product |
| 428 | * |
| 429 | * @return ?string |
| 430 | */ |
| 431 | public static function get_manage_url() { |
| 432 | // check standalone first |
| 433 | if ( static::is_standalone_plugin_active() ) { |
| 434 | return admin_url( 'admin.php?page=jetpack-backup' ); |
| 435 | // otherwise, check for the main Jetpack plugin |
| 436 | } elseif ( static::is_jetpack_plugin_active() ) { |
| 437 | return Redirect::get_url( 'my-jetpack-manage-backup' ); |
| 438 | } |
| 439 | } |
| 440 | |
| 441 | /** |
| 442 | * Get the product-slugs of the paid plans for this product. |
| 443 | * (Do not include bundle plans, unless it's a bundle plan itself). |
| 444 | * |
| 445 | * @return array |
| 446 | */ |
| 447 | public static function get_paid_plan_product_slugs() { |
| 448 | return array( |
| 449 | 'jetpack_backup_daily', |
| 450 | 'jetpack_backup_daily_monthly', |
| 451 | 'jetpack_backup_realtime', |
| 452 | 'jetpack_backup_realtime_monthly', |
| 453 | 'jetpack_backup_t1_yearly', |
| 454 | 'jetpack_backup_t1_monthly', |
| 455 | 'jetpack_backup_t1_bi_yearly', |
| 456 | 'jetpack_backup_t2_yearly', |
| 457 | 'jetpack_backup_t2_monthly', |
| 458 | 'jetpack_backup_t0_yearly', |
| 459 | 'jetpack_backup_t0_monthly', |
| 460 | ); |
| 461 | } |
| 462 | |
| 463 | /** |
| 464 | * Override the product status to return INACTIVE when backups are deactivated. |
| 465 | * |
| 466 | * @return string |
| 467 | */ |
| 468 | public static function get_status() { |
| 469 | // Get the default status from parent. |
| 470 | $status = parent::get_status(); |
| 471 | |
| 472 | // Check if backups are deactivated (not an error, just manually turned off). |
| 473 | $needs_attention = static::does_module_need_attention(); |
| 474 | if ( |
| 475 | is_array( $needs_attention ) && |
| 476 | isset( $needs_attention['data']['status'] ) && |
| 477 | 'backups-deactivated' === $needs_attention['data']['status'] |
| 478 | ) { |
| 479 | // Preserve NEEDS_PLAN status - user must purchase before reactivating. |
| 480 | if ( \Automattic\Jetpack\My_Jetpack\Products::STATUS_NEEDS_PLAN === $status ) { |
| 481 | return $status; |
| 482 | } |
| 483 | |
| 484 | return \Automattic\Jetpack\My_Jetpack\Products::STATUS_INACTIVE; |
| 485 | } |
| 486 | |
| 487 | return $status; |
| 488 | } |
| 489 | } |