atarim-visual-collaboration
/
third-party
/
backup
/
jetbackup
/
class-avcf-abilities-jetbackup-restore.php
class-avcf-abilities-jetbackup-backups.php
2 days ago
class-avcf-abilities-jetbackup-destinations.php
2 weeks ago
class-avcf-abilities-jetbackup-jobs.php
2 weeks ago
class-avcf-abilities-jetbackup-queue.php
2 days ago
class-avcf-abilities-jetbackup-restore-point.php
2 days ago
class-avcf-abilities-jetbackup-restore.php
2 days ago
class-avcf-abilities-jetbackup-schedules.php
2 weeks ago
class-avcf-abilities-jetbackup-settings.php
2 weeks ago
class-avcf-abilities-jetbackup-system.php
2 weeks ago
class-avcf-jetbackup-detector.php
2 weeks ago
class-avcf-jetbackup-helpers.php
2 days ago
class-avcf-abilities-jetbackup-restore.php
263 lines
| 1 | <?php |
| 2 | /** |
| 3 | * JetBackup — Restore ability cluster. |
| 4 | * |
| 5 | * The highest-stakes JetBackup surface: |
| 6 | * - jetbackup-restore-backup initiate a restore of an existing snapshot |
| 7 | * - jetbackup-get-queue-item read restore progress AND the standalone |
| 8 | * restore_url (the "recover even if the site is |
| 9 | * broken" link) once it is generated |
| 10 | * - jetbackup-get-restore-settings / jetbackup-manage-restore-settings |
| 11 | * |
| 12 | * Restore is by snapshot id only. JetBackup's file-upload / import-by-path |
| 13 | * branches need a multipart upload, which isn't expressible as an MCP ability, |
| 14 | * so they're intentionally not exposed here. |
| 15 | * |
| 16 | * The raw restoreOptions bitmask is hidden behind friendly enums |
| 17 | * (entire/include/exclude/skip for files and database) that we compile into the |
| 18 | * bitmask server-side — safer for an agent than hand-building a bitmask. |
| 19 | * |
| 20 | * Gated by manage_options; restore requires confirm:true. Built on JetBackup |
| 21 | * 3.1.23.x; not runtime-tested here. |
| 22 | * |
| 23 | * @package atarim-visual-collaboration |
| 24 | */ |
| 25 | |
| 26 | if ( ! defined( 'ABSPATH' ) ) { |
| 27 | exit; |
| 28 | } |
| 29 | |
| 30 | class AVCF_Abilities_JetBackup_Restore extends AVCF_Abilities_Base { |
| 31 | |
| 32 | /** @var AVCF_JetBackup_Detector */ |
| 33 | private $detector; |
| 34 | |
| 35 | public function __construct() { |
| 36 | $this->detector = new AVCF_JetBackup_Detector(); |
| 37 | } |
| 38 | |
| 39 | public function register() { |
| 40 | if ( ! $this->detector->avcf_jetbackup_is_available() ) { |
| 41 | return; |
| 42 | } |
| 43 | $this->register_restore_backup(); |
| 44 | $this->register_get_queue_item(); |
| 45 | $this->register_get_restore_settings(); |
| 46 | $this->register_manage_restore_settings(); |
| 47 | } |
| 48 | |
| 49 | /* ------------------------------ shared ----------------------------- */ |
| 50 | |
| 51 | private function ro_meta() { |
| 52 | return [ 'mcp' => [ 'public' => true, 'type' => 'tool' ], 'annotations' => [ 'readonly' => true, 'destructive' => false, 'idempotent' => true ] ]; |
| 53 | } |
| 54 | private function write_meta( $d = false ) { |
| 55 | return [ 'mcp' => [ 'public' => true, 'type' => 'tool' ], 'annotations' => [ 'readonly' => false, 'destructive' => (bool) $d, 'idempotent' => false ] ]; |
| 56 | } |
| 57 | public function can() { |
| 58 | return AVCF_JetBackup_Helpers::can_manage(); |
| 59 | } |
| 60 | private function out_schema() { |
| 61 | return [ |
| 62 | 'type' => 'object', |
| 63 | 'properties' => [ |
| 64 | 'success' => [ 'type' => 'boolean' ], |
| 65 | 'message' => [ 'type' => 'string' ], |
| 66 | 'data' => [ 'type' => 'object' ], |
| 67 | ], |
| 68 | 'required' => [ 'success', 'message' ], |
| 69 | ]; |
| 70 | } |
| 71 | |
| 72 | /** |
| 73 | * Compile the friendly files/database enums into JetBackup's restoreOptions |
| 74 | * bitmask. Defaults to a full restore (entire files + entire database). |
| 75 | */ |
| 76 | private function compute_restore_options( $files, $db ) { |
| 77 | $map_files = [ |
| 78 | 'entire' => AVCF_JetBackup_Helpers::R_FILES_ENTIRE, |
| 79 | 'include' => AVCF_JetBackup_Helpers::R_FILES_INCLUDE, |
| 80 | 'exclude' => AVCF_JetBackup_Helpers::R_FILES_EXCLUDE, |
| 81 | 'skip' => AVCF_JetBackup_Helpers::R_FILES_SKIP, |
| 82 | ]; |
| 83 | $map_db = [ |
| 84 | 'entire' => AVCF_JetBackup_Helpers::R_DB_ENTIRE, |
| 85 | 'include' => AVCF_JetBackup_Helpers::R_DB_INCLUDE, |
| 86 | 'exclude' => AVCF_JetBackup_Helpers::R_DB_EXCLUDE, |
| 87 | 'skip' => AVCF_JetBackup_Helpers::R_DB_SKIP, |
| 88 | ]; |
| 89 | $f = isset( $map_files[ $files ] ) ? $map_files[ $files ] : AVCF_JetBackup_Helpers::R_FILES_ENTIRE; |
| 90 | $d = isset( $map_db[ $db ] ) ? $map_db[ $db ] : AVCF_JetBackup_Helpers::R_DB_ENTIRE; |
| 91 | return $f | $d; |
| 92 | } |
| 93 | |
| 94 | /* ---------------------------- restore-backup ------------------------ */ |
| 95 | |
| 96 | private function register_restore_backup() { |
| 97 | $self = $this; |
| 98 | $output_schema = $this->out_schema(); |
| 99 | $output_schema['properties']['queue_item_id'] = [ |
| 100 | 'type' => 'integer', |
| 101 | 'description' => 'The queue item id of the restore just started — poll jetbackup-get-queue-item with this to track progress and read restore_url once available.', |
| 102 | ]; |
| 103 | |
| 104 | wp_register_ability( 'atarim/jetbackup-restore-backup', [ |
| 105 | 'label' => 'Restore JetBackup Backup', |
| 106 | 'category' => 'atarim', |
| 107 | 'description' => 'Initiate a restore of an existing backup snapshot (by id, or by snapshot_name from a completed backup). DESTRUCTIVE: this overwrites the live site with the backup\'s contents; requires confirm:true. Only "account" backups are restorable. restore_files / restore_database choose entire|include|exclude|skip (default entire = full restore); pass folders / tables when using include or exclude. After queuing, poll jetbackup-get-queue-item to read progress and the standalone restore_url (usable to complete the restore even if the site later breaks).', |
| 108 | 'input_schema' => [ |
| 109 | 'type' => 'object', |
| 110 | 'properties' => [ |
| 111 | 'id' => [ 'type' => 'integer', 'minimum' => 1, 'description' => 'Backup snapshot id to restore. Provide this OR snapshot_name.' ], |
| 112 | 'snapshot_name' => [ 'type' => 'string', 'description' => 'Snapshot name to restore (alternative to id) — e.g. the snapshot_name returned by a completed run-backup queue item. Resolved to the snapshot id automatically.' ], |
| 113 | 'restore_files' => [ 'type' => 'string', 'enum' => [ 'entire', 'include', 'exclude', 'skip' ], 'default' => 'entire', 'description' => 'How to restore files.' ], |
| 114 | 'restore_database' => [ 'type' => 'string', 'enum' => [ 'entire', 'include', 'exclude', 'skip' ], 'default' => 'entire', 'description' => 'How to restore the database.' ], |
| 115 | 'folders' => [ 'type' => 'array', 'items' => [ 'type' => 'string' ], 'description' => 'Home-dir folders/files to include or exclude (when restore_files is include/exclude).' ], |
| 116 | 'tables' => [ 'type' => 'array', 'items' => [ 'type' => 'string' ], 'description' => 'Database tables to include or exclude (when restore_database is include/exclude).' ], |
| 117 | 'restore_options' => [ 'type' => 'integer', 'description' => 'Advanced: raw JetBackup restoreOptions bitmask. Overrides restore_files/restore_database if provided.' ], |
| 118 | 'mixed_sites' => [ 'type' => 'boolean', 'description' => 'Multisite: allow mixed-site restore.' ], |
| 119 | 'confirm' => [ 'type' => 'boolean', 'description' => 'Must be true to proceed with the restore.' ], |
| 120 | ], |
| 121 | // Deliberately no 'required': this ability takes id OR snapshot_name, |
| 122 | // as its description says and its callback implements. Naming id here |
| 123 | // made the framework refuse a snapshot_name-only call before the |
| 124 | // callback could resolve it, which is how every caller that follows |
| 125 | // the documentation was rejected. The callback already answers the |
| 126 | // neither-supplied case, and more usefully than the schema does. |
| 127 | 'additionalProperties' => false, |
| 128 | ], |
| 129 | 'output_schema' => $output_schema, |
| 130 | 'execute_callback' => function( $input = [] ) use ( $self ) { |
| 131 | $input = (array) $input; |
| 132 | $snapshot_id = ! empty( $input['id'] ) ? (int) $input['id'] : 0; |
| 133 | if ( ! $snapshot_id && ! empty( $input['snapshot_name'] ) ) { |
| 134 | $snapshot_id = (int) AVCF_JetBackup_Helpers::resolve_snapshot_id_by_name( $input['snapshot_name'] ); |
| 135 | if ( ! $snapshot_id ) { |
| 136 | return [ 'success' => false, 'message' => sprintf( 'No backup snapshot found with name "%s". Use jetbackup-list-backups to find the snapshot id.', (string) $input['snapshot_name'] ), 'data' => [] ]; |
| 137 | } |
| 138 | } |
| 139 | if ( ! $snapshot_id ) { |
| 140 | return [ 'success' => false, 'message' => 'Provide either id (snapshot id) or snapshot_name (e.g. from a completed run-backup, or jetbackup-list-backups).', 'data' => [] ]; |
| 141 | } |
| 142 | if ( ! AVCF_JetBackup_Helpers::confirmed( $input ) ) { |
| 143 | return AVCF_JetBackup_Helpers::confirm_required_response( 'restore backup — overwrites the live site' ); |
| 144 | } |
| 145 | |
| 146 | $files = isset( $input['restore_files'] ) ? strtolower( (string) $input['restore_files'] ) : 'entire'; |
| 147 | $db = isset( $input['restore_database'] ) ? strtolower( (string) $input['restore_database'] ) : 'entire'; |
| 148 | $opts = isset( $input['restore_options'] ) ? (int) $input['restore_options'] : $self->compute_restore_options( $files, $db ); |
| 149 | |
| 150 | $payload = [ |
| 151 | AVCF_JetBackup_Helpers::ID_FIELD => $snapshot_id, |
| 152 | AVCF_JetBackup_Helpers::TYPE_FIELD => AVCF_JetBackup_Helpers::QUEUE_RESTORE, |
| 153 | 'restoreOptions' => $opts, |
| 154 | ]; |
| 155 | |
| 156 | if ( in_array( $files, [ 'include', 'exclude' ], true ) && ! empty( $input['folders'] ) ) { |
| 157 | $payload['folderList'] = array_values( array_map( 'strval', (array) $input['folders'] ) ); |
| 158 | } |
| 159 | if ( in_array( $db, [ 'include', 'exclude' ], true ) && ! empty( $input['tables'] ) ) { |
| 160 | $payload['selectedTables'] = array_values( array_map( 'strval', (array) $input['tables'] ) ); |
| 161 | } |
| 162 | if ( isset( $input['mixed_sites'] ) ) { |
| 163 | $payload['mixedSites'] = filter_var( $input['mixed_sites'], FILTER_VALIDATE_BOOLEAN ); |
| 164 | } |
| 165 | |
| 166 | return AVCF_JetBackup_Helpers::surface_queue_id( |
| 167 | AVCF_JetBackup_Helpers::invoke( 'AddToQueue', $payload ), |
| 168 | AVCF_JetBackup_Helpers::QUEUE_RESTORE |
| 169 | ); |
| 170 | }, |
| 171 | 'permission_callback' => function() use ( $self ) { return $self->can(); }, |
| 172 | 'meta' => $this->write_meta( true ), |
| 173 | ] ); |
| 174 | } |
| 175 | |
| 176 | /* --------------------------- get-queue-item ------------------------- */ |
| 177 | |
| 178 | private function register_get_queue_item() { |
| 179 | $self = $this; |
| 180 | wp_register_ability( 'atarim/jetbackup-get-queue-item', [ |
| 181 | 'label' => 'Get JetBackup Queue Item', |
| 182 | 'category' => 'atarim', |
| 183 | 'description' => 'Get a single JetBackup queue item by id, including status and progress. For a completed BACKUP item, data includes snapshot_name (the snapshot it produced) — pass that to jetbackup-restore-backup. For a RESTORE item, data includes restore_url — the standalone recovery link — once the item reaches the waiting-for-restore stage.', |
| 184 | 'input_schema' => [ |
| 185 | 'type' => 'object', |
| 186 | 'properties' => [ 'id' => [ 'type' => 'integer', 'minimum' => 1, 'description' => 'Queue item id.' ] ], |
| 187 | 'required' => [ 'id' ], |
| 188 | 'additionalProperties' => false, |
| 189 | ], |
| 190 | 'output_schema' => $this->out_schema(), |
| 191 | 'execute_callback' => function( $input = [] ) { |
| 192 | $input = (array) $input; |
| 193 | if ( empty( $input['id'] ) ) { |
| 194 | return [ 'success' => false, 'message' => 'Missing id (queue item id).', 'data' => [] ]; |
| 195 | } |
| 196 | return AVCF_JetBackup_Helpers::invoke( 'GetQueueItem', AVCF_JetBackup_Helpers::id_payload( $input ) ); |
| 197 | }, |
| 198 | 'permission_callback' => function() use ( $self ) { return $self->can(); }, |
| 199 | 'meta' => $this->ro_meta(), |
| 200 | ] ); |
| 201 | } |
| 202 | |
| 203 | /* ----------------------- get-restore-settings ----------------------- */ |
| 204 | |
| 205 | private function register_get_restore_settings() { |
| 206 | $self = $this; |
| 207 | wp_register_ability( 'atarim/jetbackup-get-restore-settings', [ |
| 208 | 'label' => 'Get JetBackup Restore Settings', |
| 209 | 'category' => 'atarim', |
| 210 | 'description' => 'Get JetBackup restore settings (compatibility check, allow cross-domain, alternate path, wp-content only).', |
| 211 | 'input_schema' => [ 'type' => 'object', 'properties' => new \stdClass(), 'additionalProperties' => false ], |
| 212 | 'output_schema' => $this->out_schema(), |
| 213 | 'execute_callback' => function( $input = [] ) { |
| 214 | return AVCF_JetBackup_Helpers::invoke( 'GetSettingsRestore', [] ); |
| 215 | }, |
| 216 | 'permission_callback' => function() use ( $self ) { return $self->can(); }, |
| 217 | 'meta' => $this->ro_meta(), |
| 218 | ] ); |
| 219 | } |
| 220 | |
| 221 | /* --------------------- manage-restore-settings ---------------------- */ |
| 222 | |
| 223 | private function register_manage_restore_settings() { |
| 224 | $self = $this; |
| 225 | wp_register_ability( 'atarim/jetbackup-manage-restore-settings', [ |
| 226 | 'label' => 'Manage JetBackup Restore Settings', |
| 227 | 'category' => 'atarim', |
| 228 | 'description' => 'Update JetBackup restore settings. Only the fields you pass are changed. All are booleans: compatibility_check, allow_cross_domain, alternate_path, wp_content_only.', |
| 229 | 'input_schema' => [ |
| 230 | 'type' => 'object', |
| 231 | 'properties' => [ |
| 232 | 'compatibility_check' => [ 'type' => 'boolean', 'description' => 'Run compatibility checks before restore.' ], |
| 233 | 'allow_cross_domain' => [ 'type' => 'boolean', 'description' => 'Allow restoring a backup taken on a different domain.' ], |
| 234 | 'alternate_path' => [ 'type' => 'boolean', 'description' => 'Use an alternate wp-config.php path during restore.' ], |
| 235 | 'wp_content_only' => [ 'type' => 'boolean', 'description' => 'Restore wp-content only.' ], |
| 236 | ], |
| 237 | 'additionalProperties' => false, |
| 238 | ], |
| 239 | 'output_schema' => $this->out_schema(), |
| 240 | 'execute_callback' => function( $input = [] ) { |
| 241 | $input = (array) $input; |
| 242 | $map = [ |
| 243 | 'compatibility_check' => 'RESTORE_COMPATIBILITY_CHECK', |
| 244 | 'allow_cross_domain' => 'RESTORE_ALLOW_CROSS_DOMAIN', |
| 245 | 'alternate_path' => 'RESTORE_ALTERNATE_PATH', |
| 246 | 'wp_content_only' => 'RESTORE_WP_CONTENT_ONLY', |
| 247 | ]; |
| 248 | $payload = []; |
| 249 | foreach ( $map as $friendly => $key ) { |
| 250 | if ( array_key_exists( $friendly, $input ) ) { |
| 251 | $payload[ $key ] = filter_var( $input[ $friendly ], FILTER_VALIDATE_BOOLEAN ); |
| 252 | } |
| 253 | } |
| 254 | if ( empty( $payload ) ) { |
| 255 | return [ 'success' => false, 'message' => 'No restore settings provided to change.', 'data' => [] ]; |
| 256 | } |
| 257 | return AVCF_JetBackup_Helpers::invoke( 'ManageSettingsRestore', $payload ); |
| 258 | }, |
| 259 | 'permission_callback' => function() use ( $self ) { return $self->can(); }, |
| 260 | 'meta' => $this->write_meta( false ), |
| 261 | ] ); |
| 262 | } |
| 263 | } |