'POST', 'callback' => [ $this, 'create' ], 'permission_callback' => [ $this->permission, 'is_admin' ], 'args' => [ 'label' => [ 'required' => true, 'type' => 'string' ], 'connection_id' => [ 'required' => true, 'type' => 'integer', 'minimum' => 1 ], 'direction' => [ 'required' => true, 'type' => 'string', 'enum' => [ 'pull' ] ], 'stages' => [ 'required' => false, 'type' => 'object', 'default' => [] ], ], ] ); register_rest_route( RestRegistrar::NAMESPACE, '/profile/(?P[\d]+)', [ 'methods' => 'GET', 'callback' => [ $this, 'get_one' ], 'permission_callback' => [ $this->permission, 'is_admin' ], 'args' => [ 'id' => [ 'required' => true, 'type' => 'integer', 'minimum' => 1 ], ], ] ); register_rest_route( RestRegistrar::NAMESPACE, '/profile/(?P[\d]+)', [ 'methods' => 'PATCH', 'callback' => [ $this, 'rename' ], 'permission_callback' => [ $this->permission, 'is_admin' ], 'args' => [ 'id' => [ 'required' => true, 'type' => 'integer', 'minimum' => 1 ], 'label' => [ 'required' => true, 'type' => 'string' ], ], ] ); register_rest_route( RestRegistrar::NAMESPACE, '/profile/(?P[\d]+)', [ 'methods' => 'DELETE', 'callback' => [ $this, 'delete' ], 'permission_callback' => [ $this->permission, 'is_admin' ], 'args' => [ 'id' => [ 'required' => true, 'type' => 'integer', 'minimum' => 1 ], ], ] ); register_rest_route( RestRegistrar::NAMESPACE, '/profile/(?P[\d]+)/touch', [ 'methods' => 'POST', 'callback' => [ $this, 'touch' ], 'permission_callback' => [ $this->permission, 'is_admin' ], 'args' => [ 'id' => [ 'required' => true, 'type' => 'integer', 'minimum' => 1 ], ], ] ); } // ------------------------------------------------------------------------- // Handlers // ------------------------------------------------------------------------- public function create( WP_REST_Request $request ): WP_REST_Response|WP_Error { $nonce_check = $this->require_rest_nonce( $request ); if ( is_wp_error( $nonce_check ) ) { return $nonce_check; } $connection_id = (int) $request->get_param( 'connection_id' ); // A profile pointing at a connection that doesn't exist can never be run. if ( ! $this->connections->find_by_id( $connection_id ) ) { return new WP_Error( 'shiftora_not_found', __( 'Connection not found.', 'shiftora' ), [ 'status' => 404 ] ); } try { $profile_id = $this->profiles->create( (string) $request->get_param( 'label' ), $connection_id, (string) $request->get_param( 'direction' ), $this->whole_stages( (array) $request->get_param( 'stages' ) ) ); } catch ( InvalidArgumentException $e ) { return new WP_Error( 'shiftora_invalid_profile', $e->getMessage(), [ 'status' => 422 ] ); } catch ( RuntimeException $e ) { return new WP_Error( 'shiftora_db_error', $e->getMessage(), [ 'status' => 500 ] ); } return new WP_REST_Response( [ 'success' => true, 'profile_id' => $profile_id, ], 201 ); } /** * A profile remembers which stages run, nothing about items inside them. * * @param array $stages Stage config as the wizard sent it. * * @return array */ private function whole_stages( array $stages ): array { $clean = []; foreach ( [ 'database', ...FileStages::keys() ] as $key ) { if ( is_array( $stages[ $key ] ?? null ) ) { $clean[ $key ] = [ 'enabled' => ! empty( $stages[ $key ]['enabled'] ) ]; } } return $clean; } public function get_one( WP_REST_Request $request ): WP_REST_Response|WP_Error { $profile = $this->profiles->find_by_id( (int) $request->get_param( 'id' ) ); if ( ! $profile ) { return new WP_Error( 'shiftora_not_found', __( 'Profile not found.', 'shiftora' ), [ 'status' => 404 ] ); } return new WP_REST_Response( [ 'id' => (int) $profile->id, 'label' => $profile->label, 'connection_id' => (int) $profile->connection_id, 'direction' => $profile->direction, 'stages' => $this->whole_stages( $profile->stages_json ? (array) json_decode( $profile->stages_json, true ) : [] ), ], 200 ); } public function rename( WP_REST_Request $request ): WP_REST_Response|WP_Error { $nonce_check = $this->require_rest_nonce( $request ); if ( is_wp_error( $nonce_check ) ) { return $nonce_check; } $id = (int) $request->get_param( 'id' ); if ( ! $this->profiles->find_by_id( $id ) ) { return new WP_Error( 'shiftora_not_found', __( 'Profile not found.', 'shiftora' ), [ 'status' => 404 ] ); } try { $this->profiles->rename( $id, (string) $request->get_param( 'label' ) ); } catch ( InvalidArgumentException $e ) { return new WP_Error( 'shiftora_invalid_profile', $e->getMessage(), [ 'status' => 422 ] ); } return new WP_REST_Response( [ 'success' => true ], 200 ); } public function delete( WP_REST_Request $request ): WP_REST_Response|WP_Error { $nonce_check = $this->require_rest_nonce( $request ); if ( is_wp_error( $nonce_check ) ) { return $nonce_check; } $id = (int) $request->get_param( 'id' ); if ( ! $this->profiles->find_by_id( $id ) ) { return new WP_Error( 'shiftora_not_found', __( 'Profile not found.', 'shiftora' ), [ 'status' => 404 ] ); } $this->profiles->delete( $id ); return new WP_REST_Response( [ 'success' => true ], 200 ); } public function touch( WP_REST_Request $request ): WP_REST_Response|WP_Error { $nonce_check = $this->require_rest_nonce( $request ); if ( is_wp_error( $nonce_check ) ) { return $nonce_check; } $id = (int) $request->get_param( 'id' ); if ( ! $this->profiles->find_by_id( $id ) ) { return new WP_Error( 'shiftora_not_found', __( 'Profile not found.', 'shiftora' ), [ 'status' => 404 ] ); } $this->profiles->touch_last_used( $id ); return new WP_REST_Response( [ 'success' => true ], 200 ); } /** * The admin-ajax shim dispatches straight to the REST server, skipping the cookie-auth * nonce filter. These routes are admin-browser only, so they can require a nonce * outright. Mirrors ConnectionEndpoint::require_rest_nonce(). */ private function require_rest_nonce( WP_REST_Request $request ): bool|WP_Error { $nonce = (string) ( $request->get_header( 'X-WP-Nonce' ) ?: $request->get_param( '_wpnonce' ) ); if ( '' !== $nonce && wp_verify_nonce( sanitize_text_field( wp_unslash( $nonce ) ), 'wp_rest' ) ) { return true; } return new WP_Error( 'shiftora_invalid_nonce', __( 'Security check failed. Please refresh the page and try again.', 'shiftora' ), [ 'status' => 403 ] ); } }