'GET', 'callback' => array( $this, 'estimate' ), 'permission_callback' => array( $this, 'fullsite_permission_check' ), 'args' => array( 'fresh' => array( 'description' => __( 'Measure again instead of reusing the last measurement.', 'transtrix' ), 'type' => 'boolean', 'default' => false, 'sanitize_callback' => 'rest_sanitize_boolean', ), ), ), ) ); register_rest_route( self::NAMESPACE_V1, '/export/full', array( array( 'methods' => 'POST', 'callback' => array( $this, 'start_export' ), 'permission_callback' => array( $this, 'fullsite_permission_check' ), 'args' => $this->export_args(), ), ) ); register_rest_route( self::NAMESPACE_V1, '/jobs/(?P[A-Za-z0-9_.]+)/step', array( array( 'methods' => 'POST', 'callback' => array( $this, 'step' ), 'permission_callback' => array( $this, 'fullsite_permission_check' ), 'args' => $this->step_args(), ), ) ); register_rest_route( self::NAMESPACE_V1, '/import/(?P[a-f0-9]{32})/preflight', array( array( 'methods' => 'POST', 'callback' => array( $this, 'preflight' ), 'permission_callback' => array( $this, 'fullsite_permission_check' ), 'args' => $this->package_id_arg(), ), ) ); register_rest_route( self::NAMESPACE_V1, '/import/(?P[a-f0-9]{32})/restore', array( array( 'methods' => 'POST', 'callback' => array( $this, 'start_restore' ), 'permission_callback' => array( $this, 'fullsite_permission_check' ), 'args' => array_merge( $this->package_id_arg(), $this->restore_args() ), ), ) ); } /** * What each layer would weigh. * * @param WP_REST_Request $request The request. * @return \WP_REST_Response */ public function estimate( WP_REST_Request $request ) { return rest_ensure_response( array( 'layers' => Estimator::layers( (bool) $request['fresh'] ), 'free' => Estimator::free_space( wp_upload_dir( null, false )['basedir'] ), ) ); } /** * Begin a whole-site export. * * @param WP_REST_Request $request The request. * @return \WP_REST_Response|\WP_Error */ public function start_export( WP_REST_Request $request ) { $job = new Job( 'full-site', array() ); $exporter = new Full_Site_Exporter( $job->id() ); $cursor = $exporter->plan( array( 'layers' => (array) $request['layers'], ) ); if ( is_wp_error( $cursor ) ) { return $this->from_engine( $cursor ); } Step_State::save( $job->id(), $cursor ); $record = Job_Store::save( $job, array( 'type' => 'export', 'kind' => 'full-site', 'task' => $exporter->id(), 'status' => 'running', 'phase' => (string) $cursor['phase'], 'package_id' => (string) $cursor['package_id'], 'total' => (int) $cursor['total'], ) ); return rest_ensure_response( $record ); } /** * Advance a stepped job by one step. * * @param WP_REST_Request $request The request. * @return \WP_REST_Response|\WP_Error */ public function step( WP_REST_Request $request ) { $job_id = (string) $request['id']; $task = Tasks::for_job( $job_id ); if ( null === $task ) { return $this->error( 'transtrix_job_not_steppable', __( 'This can’t be continued. Start it again.', 'transtrix' ), 400 ); } $result = Step_Loop::advance( $job_id, $task ); if ( is_wp_error( $result ) ) { return $this->from_engine( $result ); } if ( ! empty( $result['complete'] ) ) { $this->deliver( $job_id ); // The step shape carries progress only; the finished package's // counts and name live on the job record. $record = Job_Store::get( $job_id ); if ( null !== $record ) { $result['counts'] = isset( $record['counts'] ) ? $record['counts'] : array(); $result['package_name'] = Package_Store::display_name( $record ); } } return rest_ensure_response( $result ); } /** * What this site would say about an archive before restoring it. * * @param WP_REST_Request $request The request. * @return \WP_REST_Response|\WP_Error */ public function preflight( WP_REST_Request $request ) { $path = Package_Store::path( (string) $request['package_id'] ); if ( is_wp_error( $path ) ) { return $this->from_engine( $path ); } $manifest = Package_Reader::peek( $path ); if ( is_wp_error( $manifest ) ) { return $this->from_engine( $manifest ); } if ( 'full-site' !== $manifest['kind'] ) { return $this->error( 'transtrix_not_full_site', __( 'That is not a whole-site package.', 'transtrix' ), 400 ); } $findings = Environment_Guard::check( $manifest, (int) ( isset( $manifest['totals']['bytes'] ) ? $manifest['totals']['bytes'] : 0 ) ); return rest_ensure_response( array( 'kind' => 'full-site', 'layers' => isset( $manifest['layers'] ) ? $manifest['layers'] : array(), 'source' => isset( $manifest['environment'] ) ? $manifest['environment'] : array(), 'snapshot' => isset( $manifest['snapshot'] ) ? $manifest['snapshot'] : array(), 'totals' => isset( $manifest['totals'] ) ? $manifest['totals'] : array(), 'findings' => $findings, 'blocked' => Environment_Guard::blocked( $findings ), ) ); } /** * Begin a whole-site restore. * * @param WP_REST_Request $request The request. * @return \WP_REST_Response|\WP_Error */ public function start_restore( WP_REST_Request $request ) { $job = new Job( 'full-site', array() ); $restorer = new Full_Site_Restorer( $job->id() ); // Restoring plugins or themes installs their code, so it needs the same // capability as installing them. Dropped the way DISALLOW_FILE_MODS drops them. $layers = array_values( array_diff( (array) $request['layers'], current_user_can( 'install_plugins' ) ? array() : array( Layers::PLUGINS ), current_user_can( 'install_themes' ) ? array() : array( Layers::THEMES ) ) ); $cursor = $restorer->plan( array( 'package_id' => (string) $request['package_id'], 'layers' => $layers, 'rewrite_to' => home_url(), ) ); if ( is_wp_error( $cursor ) ) { return $this->from_engine( $cursor ); } Step_State::save( $job->id(), $cursor ); $record = Job_Store::save( $job, array( 'type' => 'import', 'kind' => 'full-site', 'task' => $restorer->id(), 'status' => 'running', 'phase' => (string) $cursor['phase'], 'package_id' => (string) $cursor['package_id'], 'total' => (int) $cursor['total'], ) ); return rest_ensure_response( $record ); } /** * Hand a finished export to its destination. * * @param string $job_id Job id. * @return void */ private function deliver( string $job_id ): void { $record = Job_Store::get( $job_id ); if ( null === $record || 'export' !== $record['type'] || '' === $record['package_id'] ) { return; } $path = Package_Store::path( (string) $record['package_id'] ); if ( is_wp_error( $path ) ) { return; } $written = ( new File_Destination() )->write( $path, array( 'package_id' => (string) $record['package_id'], 'kind' => 'full-site', ) ); if ( ! is_wp_error( $written ) ) { Job_Store::update( $job_id, array( 'destination' => $written ) ); } } /** * Arguments for starting an export. * * @return array> */ private function export_args(): array { return array( 'layers' => array( 'description' => __( 'Which layers to archive.', 'transtrix' ), 'type' => 'array', 'default' => Layers::ids(), 'items' => array( 'type' => 'string' ), 'sanitize_callback' => static function ( $value ) { return Layers::filter( (array) $value ); }, 'validate_callback' => static function ( $value ) { return array() !== Layers::filter( (array) $value ); }, ), ); } /** * Arguments for starting a restore. * * @return array> */ private function restore_args(): array { return array( 'layers' => array( 'description' => __( 'Which layers to restore.', 'transtrix' ), 'type' => 'array', 'default' => Layers::ids(), 'items' => array( 'type' => 'string' ), 'sanitize_callback' => static function ( $value ) { return Layers::filter( (array) $value ); }, 'validate_callback' => static function ( $value ) { return array() !== Layers::filter( (array) $value ); }, ), ); } /** * Arguments for a step. * * @return array> */ private function step_args(): array { return array( 'id' => array( 'description' => __( 'Job id.', 'transtrix' ), 'type' => 'string', 'required' => true, 'sanitize_callback' => static function ( $value ) { return preg_replace( '/[^A-Za-z0-9_.]/', '', (string) $value ); }, ), 'token' => array( 'description' => __( 'The token returned by the previous step.', 'transtrix' ), 'type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field', ), ); } }