[\d]+)/ping', [ 'methods' => 'POST', 'callback' => [ $this, 'connection_ping' ], 'permission_callback' => [ $this->permission, 'is_admin' ], 'args' => [ 'id' => [ 'required' => true, 'type' => 'integer', 'minimum' => 1 ], ], ] ); register_rest_route( 'shiftora/v1', '/connection/(?P[\d]+)', [ 'methods' => 'DELETE', 'callback' => [ $this, 'connection_delete' ], 'permission_callback' => [ $this->permission, 'is_admin' ], 'args' => [ 'id' => [ 'required' => true, 'type' => 'integer', 'minimum' => 1 ], ], ] ); register_rest_route( 'shiftora/v1', '/backup/cleanup', [ 'methods' => 'POST', 'callback' => [ $this, 'backup_cleanup' ], 'permission_callback' => [ $this->permission, 'is_admin' ], ] ); register_rest_route( 'shiftora/v1', '/backup/(?P[\d]+)', [ 'methods' => 'DELETE', 'callback' => [ $this, 'backup_delete' ], 'permission_callback' => [ $this->permission, 'is_admin' ], 'args' => [ 'id' => [ 'required' => true, 'type' => 'integer', 'minimum' => 1 ], ], ] ); } // ------------------------------------------------------------------------- // Handlers // ------------------------------------------------------------------------- public function connection_ping( WP_REST_Request $request ): WP_REST_Response|WP_Error { $id = (int) $request->get_param( 'id' ); $conn = $this->connections->find_by_id( $id ); if ( ! $conn ) { return new WP_Error( 'shiftora_not_found', __( 'Connection not found.', 'shiftora' ), [ 'status' => 404 ] ); } $validator = new ConnectionValidator(); try { $ping = $validator->ping( $conn->remote_url ); $this->connections->update_last_ping( $id ); $this->connections->set_remote_permissions( $id, isset( $ping['allow_pull'] ) ? (int) (bool) $ping['allow_pull'] : 1 ); } catch ( \RuntimeException $e ) { $remote_error = RemoteErrorExplainer::to_wp_error( $e, $id ); if ( null !== $remote_error ) { ServerLog::write( sprintf( '[Shiftora] ping failed for connection %d, %s', $id, $e->getMessage() ) ); return $remote_error; } return new WP_Error( 'shiftora_ping_failed', $e->getMessage(), [ 'status' => 500 ] ); } return new WP_REST_Response( [ 'success' => true, 'latency_ms' => $ping['latency_ms'] ?? null, // A version rather than a flag, so the UI can tell an older destination apart. 'transfer_protocol' => (int) ( $ping['transfer_protocol'] ?? 0 ), 'allow_pull' => $ping['allow_pull'] ?? true, ], 200 ); } public function connection_delete( WP_REST_Request $request ): WP_REST_Response|WP_Error { global $wpdb; $id = (int) $request->get_param( 'id' ); // Orphaned jobs would fail with "connection not found". Paused runs resume against // this connection, and a cancel still settling needs it to reach the destination. // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- custom table, fresh read required $active = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM %i WHERE connection_id = %d AND status IN ('running', 'pending', 'paused', 'cancelling')", $wpdb->prefix . 'shiftora_migrations', $id ) ); if ( $active > 0 ) { return new WP_Error( 'shiftora_connection_in_use', __( 'This connection has a migration that is running, paused or still canceling. Finish or cancel it first.', 'shiftora' ), [ 'status' => 409 ] ); } if ( ! $this->connections->delete( $id ) ) { return new WP_Error( 'shiftora_delete_failed', __( 'Failed to delete connection.', 'shiftora' ), [ 'status' => 500 ] ); } return new WP_REST_Response( [ 'success' => true ], 200 ); } public function backup_cleanup( WP_REST_Request $request ): WP_REST_Response|WP_Error { global $wpdb; $cleaner = new BackupCleaner( $this->backups, $wpdb ); $cleaner->clean(); return new WP_REST_Response( [ 'success' => true ], 200 ); } public function backup_delete( WP_REST_Request $request ): WP_REST_Response|WP_Error { $id = (int) $request->get_param( 'id' ); $this->backups->delete( $id ); return new WP_REST_Response( [ 'success' => true ], 200 ); } }