'GET', 'timeout' => $timeout, 'headers' => self::headers( $api_key ), ]; return self::request( $url, $args ); } /** * Perform a POST request with JSON body. * * @param string $path API path. * @param string|null $api_key API key. * @param array $body Request body (will be JSON-encoded). * @param array $extra_headers Additional headers. * @param int $timeout Request timeout in seconds. * @param bool $blocking Wait for and return the response. False fires the * request and returns immediately, without reading a reply. * @return array|WP_Error */ public static function post( $path, $api_key = null, $body = [], $extra_headers = [], $timeout = 60, $blocking = true ) { $args = [ 'method' => 'POST', 'timeout' => $timeout, 'blocking' => $blocking, 'headers' => array_merge( self::headers( $api_key ), [ 'Content-Type' => 'application/json' ], $extra_headers ), 'body' => wp_json_encode( $body ), ]; return self::request( self::url( $path ), $args ); } /** * Perform a POST request with multipart/form-data. * * @param string $path API path. * @param string|null $api_key API key. * @param array $fields Form field key-value pairs. * @return array|WP_Error */ public static function post_form( $path, $api_key = null, $fields = [] ) { $boundary = wp_generate_password( 24, false ); $body = ''; foreach ( $fields as $name => $value ) { $body .= "--{$boundary}\r\n"; $body .= "Content-Disposition: form-data; name=\"{$name}\"\r\n\r\n"; $body .= "{$value}\r\n"; } $body .= "--{$boundary}--\r\n"; $args = [ 'method' => 'POST', 'timeout' => 120, 'headers' => array_merge( self::headers( $api_key ), [ 'Content-Type' => "multipart/form-data; boundary={$boundary}" ] ), 'body' => $body, ]; return self::request( self::url( $path ), $args ); } /** * Perform a PUT request with JSON body. * * @param string $path API path. * @param string|null $api_key API key. * @param array $body Request body (will be JSON-encoded). * @param int $timeout Request timeout in seconds. * @return array|WP_Error */ public static function put( $path, $api_key = null, $body = [], $timeout = 60 ) { $args = [ 'method' => 'PUT', 'timeout' => $timeout, 'headers' => array_merge( self::headers( $api_key ), [ 'Content-Type' => 'application/json' ] ), 'body' => wp_json_encode( $body ), ]; return self::request( self::url( $path ), $args ); } /** * Perform a PATCH request with JSON body. * * @param string $path API path. * @param string|null $api_key API key. * @param array $body Request body. * @return array|WP_Error */ public static function patch( $path, $api_key = null, $body = [] ) { $args = [ 'method' => 'PATCH', 'timeout' => 30, 'headers' => array_merge( self::headers( $api_key ), [ 'Content-Type' => 'application/json' ] ), 'body' => wp_json_encode( $body ), ]; return self::request( self::url( $path ), $args ); } /** * Perform a DELETE request. * * @param string $path API path. * @param string|null $api_key API key. * @param array $body Optional body. * @return array|WP_Error */ public static function delete( $path, $api_key = null, $body = [] ) { $args = [ 'method' => 'DELETE', 'timeout' => 30, 'headers' => array_merge( self::headers( $api_key ), [ 'Content-Type' => 'application/json' ] ), 'body' => ! empty( $body ) ? wp_json_encode( $body ) : null, ]; return self::request( self::url( $path ), $args ); } /** * Build common headers. * * @param string|null $api_key * @return array */ private static function headers( $api_key = null ) { $headers = [ 'Accept' => 'application/json', // Lets the Mintall API tell the plugin's own sync fetches apart // from real visitors. Matched by substring server-side. 'User-Agent' => 'mintall-woocommerce-app/' . MINTALL_KEEP_VERSION, ]; if ( $api_key ) { $headers['X-API-KEY'] = $api_key; } return $headers; } /** * Execute request and decode response. * * @param string $url Full URL. * @param array $args wp_remote_request args. * @return array|WP_Error Decoded body array or WP_Error. */ private static function request( $url, $args ) { $response = wp_remote_request( $url, $args ); if ( is_wp_error( $response ) ) { return $response; } $code = wp_remote_retrieve_response_code( $response ); $body = wp_remote_retrieve_body( $response ); $data = json_decode( $body, true ); if ( $code >= 400 ) { $detail = ''; if ( is_array( $data ) && isset( $data['detail'] ) ) { $detail = is_string( $data['detail'] ) ? $data['detail'] : wp_json_encode( $data['detail'] ); } // The backend rejects work for an account whose signing keypair it // can't reproduce with a 409 REAUTH_REQUIRED. Flag it here so any // call (e.g. enqueue from the product page) trips the banner without // waiting for the next dashboard load to reconcile. // A 401/403 means the key this store holds is no longer accepted. // Without this branch the error degrades into empty results — mint // reports UNPROTECTED, protection returns an empty product list — // so the merchant sees a store that looks connected with nothing in // it, instead of a reconnect prompt. The flag is self-correcting: // every account read reconciles it from the backend's status, so a // transient rejection clears itself on the next dashboard load. if ( 'REAUTH_REQUIRED' === $detail || in_array( (int) $code, [ 401, 403 ], true ) ) { Connection_Manager::flag_reauth_required(); } return new \WP_Error( 'mintall_api_error', $detail ?: "HTTP {$code}", [ 'status' => $code, 'body' => $data, ] ); } return $data ?: []; } }