and POST /jobs//cancel. * * @package Transtrix */ namespace Transtrix\Rest\Controllers; use Transtrix\Runner\Job_Store; use WP_REST_Request; defined( 'ABSPATH' ) || exit; /** * The endpoint the UI polls while a job runs. */ class Jobs extends Controller { /** * Phases during which a job refuses to be cancelled. * * Mirrors Steppable::uncancellable_phases(). Kept as a constant here so the * cancel route does not have to instantiate the task to answer a question * about it. */ const UNCANCELLABLE_PHASES = array( 'swap' ); /** * Register routes. * * @return void */ public function register_routes(): void { register_rest_route( self::NAMESPACE_V1, '/jobs/(?P[A-Za-z0-9_.]+)', array( array( 'methods' => 'GET', 'callback' => array( $this, 'get_job' ), 'permission_callback' => array( $this, 'permission_check' ), 'args' => $this->job_id_arg(), ), array( 'methods' => 'DELETE', 'callback' => array( $this, 'delete_job' ), 'permission_callback' => array( $this, 'permission_check' ), 'args' => $this->job_id_arg(), ), ) ); register_rest_route( self::NAMESPACE_V1, '/jobs/(?P[A-Za-z0-9_.]+)/cancel', array( array( 'methods' => 'POST', 'callback' => array( $this, 'cancel_job' ), 'permission_callback' => array( $this, 'permission_check' ), 'args' => $this->job_id_arg(), ), ) ); } /** * One job's status. * * @param WP_REST_Request $request The request. * @return \WP_REST_Response|\WP_Error */ public function get_job( WP_REST_Request $request ) { $record = Job_Store::get( (string) $request['id'] ); if ( null === $record ) { return $this->error( 'transtrix_job_not_found', __( 'That export or import no longer exists. It may have been deleted from History.', 'transtrix' ), 404 ); } if ( ! $this->may_touch( $record ) ) { return $this->full_site_forbidden(); } return rest_ensure_response( $record ); } /** * Remove a finished job from History, with its stored package. * * @param WP_REST_Request $request The request. * @return \WP_REST_Response|\WP_Error */ public function delete_job( WP_REST_Request $request ) { if ( ! $this->may_touch( Job_Store::get( (string) $request['id'] ) ) ) { return $this->full_site_forbidden(); } if ( ! Job_Store::delete( (string) $request['id'] ) ) { return $this->error( 'transtrix_job_not_deletable', __( 'This is still running or was already deleted. If it is running, stop it first.', 'transtrix' ), 409 ); } return rest_ensure_response( array( 'deleted' => true ) ); } /** * Cancel a job that has not finished. * * @param WP_REST_Request $request The request. * @return \WP_REST_Response|\WP_Error */ public function cancel_job( WP_REST_Request $request ) { $job_id = (string) $request['id']; $record = Job_Store::get( $job_id ); if ( ! $this->may_touch( $record ) ) { return $this->full_site_forbidden(); } // A whole-site restore that has started swapping tables into place // cannot be stopped half way: the swap is one statement, and the only // way back from a partial database is the restore point taken before // it started. Offering a cancel button here would be a lie. if ( null !== $record && ! empty( $record['phase'] ) && in_array( $record['phase'], self::UNCANCELLABLE_PHASES, true ) ) { return $this->error( 'transtrix_job_past_no_return', __( 'This restore is writing the site into place and can no longer be stopped.', 'transtrix' ), 409 ); } $record = Job_Store::cancel( $job_id ); if ( null === $record ) { return $this->error( 'transtrix_job_not_cancellable', __( 'That has already finished, so there is nothing to cancel.', 'transtrix' ), 409 ); } return rest_ensure_response( $record ); } /** * Whether the current user may act on a job. Whole-site jobs are admin-only. * * @param array|null $record The job record, or null. * @return bool */ private function may_touch( ?array $record ): bool { return null === $record || ( ( 'full-site' !== ( $record['kind'] ?? '' ) || $this->can_access_full_site() ) && $this->owns_job( $record ) ); } /** * The job id argument. * * @return array> */ private function job_id_arg(): 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 ); }, 'validate_callback' => static function ( $value ) { return 1 === preg_match( '/^[A-Za-z0-9_.]{1,64}$/', (string) $value ); }, ), ); } }