diff --git a/woolist-phplist/includes/class-woolist-api.php b/woolist-phplist/includes/class-woolist-api.php index 42862da..3cfe483 100644 --- a/woolist-phplist/includes/class-woolist-api.php +++ b/woolist-phplist/includes/class-woolist-api.php @@ -2,6 +2,10 @@ /** * phpList REST API wrapper. * + * The phpList REST API v3 expects a POST request with ALL parameters + * (including login, password, cmd) in the POST body — NOT the query string. + * The endpoint URL itself only needs ?page=call&pi=restapi. + * * @package WooList */ @@ -12,7 +16,7 @@ class WooList_API { /** * Retrieve a saved plugin option. * - * @param string $key Option key (without woolist_ prefix). + * @param string $key Option key without the woolist_ prefix. * @param mixed $default Default value. * @return mixed */ @@ -21,55 +25,60 @@ class WooList_API { } /** - * Build the full phpList REST API URL. + * Return the base API endpoint URL (no credentials, no command). * - * All parameters are passed as query-string arguments because the phpList - * REST API reads them from $_GET regardless of the HTTP method used. - * - * @param string $cmd phpList API command. - * @param array $extra Additional query parameters. - * @return string|WP_Error Full URL or WP_Error when base URL is missing. + * @return string|WP_Error */ - public function build_url( string $cmd, array $extra = [] ) { - $base = $this->get_option( 'phplist_url' ); - $base = rtrim( $base, '/' ); + private function endpoint_url() { + $base = rtrim( (string) $this->get_option( 'phplist_url' ), '/' ); if ( empty( $base ) ) { return new WP_Error( 'woolist_no_url', __( 'phpList base URL is not configured.', 'woolist-phplist' ) ); } - $params = array_merge( + return $base . '/admin/?page=call&pi=restapi'; + } + + /** + * Execute a phpList REST API call. + * + * Credentials and all command parameters are sent in the POST body so + * phpList can authenticate and parse the request correctly. + * + * @param string $cmd phpList API command (e.g. subscriberAdd). + * @param array $extra Additional POST body parameters. + * @return array|WP_Error Decoded JSON data or WP_Error. + */ + public function call( string $cmd, array $extra = [] ) { + $url = $this->endpoint_url(); + + if ( is_wp_error( $url ) ) { + WooList_Logger::error( 'Cannot build endpoint URL for cmd=' . $cmd . ': ' . $url->get_error_message() ); + return $url; + } + + // Build the POST body — credentials + command + any extra params. + $body = array_merge( [ - 'page' => 'call', - 'pi' => 'restapi', - 'login' => $this->get_option( 'phplist_login' ), - 'password' => $this->get_option( 'phplist_password' ), + 'login' => (string) $this->get_option( 'phplist_login' ), + 'password' => (string) $this->get_option( 'phplist_password' ), 'cmd' => $cmd, ], $extra ); - return $base . '/admin/?' . http_build_query( $params ); - } + // Log the outgoing request at debug level; redact the password. + $logged_body = $body; + $logged_body['password'] = '***'; + WooList_Logger::debug( '→ API request cmd=' . $cmd . ' endpoint=' . $url . ' body=' . wp_json_encode( $logged_body ) ); - /** - * Execute an API call and return decoded JSON data. - * - * @param string $cmd phpList API command. - * @param array $extra Additional query parameters. - * @return array|WP_Error Decoded response data or WP_Error. - */ - public function call( string $cmd, array $extra = [] ) { - $url = $this->build_url( $cmd, $extra ); - - if ( is_wp_error( $url ) ) { - WooList_Logger::error( 'Cannot build URL for cmd=' . $cmd . ': ' . $url->get_error_message() ); - return $url; - } - - WooList_Logger::debug( '→ API request cmd=' . $cmd . ' url=' . WooList_Logger::redact_url( $url ) ); - - $response = wp_remote_post( $url, [ 'timeout' => 15 ] ); + $response = wp_remote_post( + $url, + [ + 'timeout' => 15, + 'body' => $body, + ] + ); if ( is_wp_error( $response ) ) { WooList_Logger::error( 'HTTP request failed cmd=' . $cmd . ' error=' . $response->get_error_message() ); @@ -77,31 +86,33 @@ class WooList_API { } $code = wp_remote_retrieve_response_code( $response ); - $body = wp_remote_retrieve_body( $response ); + $body_raw = wp_remote_retrieve_body( $response ); - // Log the raw response at debug level (truncated to 2 KB to avoid huge entries). - WooList_Logger::debug( '← API response cmd=' . $cmd . ' http=' . $code . ' body=' . substr( $body, 0, 2048 ) ); + // Always log the raw response at DEBUG so problems are immediately visible. + WooList_Logger::debug( '← API response cmd=' . $cmd . ' http=' . $code . ' body=' . substr( $body_raw, 0, 2048 ) ); if ( $code < 200 || $code >= 300 ) { - WooList_Logger::error( 'API returned HTTP ' . $code . ' cmd=' . $cmd ); + WooList_Logger::error( 'Non-2xx HTTP response cmd=' . $cmd . ' http=' . $code . ' body=' . substr( $body_raw, 0, 500 ) ); return new WP_Error( 'woolist_http_error', 'HTTP error ' . $code ); } - $data = json_decode( $body, true ); + $data = json_decode( $body_raw, true ); if ( json_last_error() !== JSON_ERROR_NONE ) { - WooList_Logger::error( 'Invalid JSON response cmd=' . $cmd . ' body=' . substr( $body, 0, 500 ) ); - return new WP_Error( 'woolist_json_error', 'Invalid JSON response from phpList.' ); + // Not JSON — likely an HTML error page or wrong endpoint. + WooList_Logger::error( 'Non-JSON response cmd=' . $cmd . ' body=' . substr( $body_raw, 0, 500 ) ); + return new WP_Error( 'woolist_json_error', 'phpList did not return JSON. Check endpoint URL and credentials.' ); } - // phpList REST API signals errors via a "status" field. + // phpList signals errors via status=error; message may be in several fields. if ( isset( $data['status'] ) && strtolower( $data['status'] ) === 'error' ) { - $message = $data['errormessage'] ?? $data['message'] ?? 'Unknown API error'; - WooList_Logger::error( 'API error cmd=' . $cmd . ' message=' . $message ); - return new WP_Error( 'woolist_api_error', $message ); + $message = $data['errormessage'] ?? $data['message'] ?? $data['data'] ?? 'Unknown API error'; + // Also dump the full response so the admin can see exactly what phpList said. + WooList_Logger::error( 'API error cmd=' . $cmd . ' message=' . $message . ' full_response=' . wp_json_encode( $data ) ); + return new WP_Error( 'woolist_api_error', (string) $message ); } - WooList_Logger::debug( 'API call succeeded cmd=' . $cmd ); + WooList_Logger::debug( 'API call succeeded cmd=' . $cmd . ' response=' . wp_json_encode( $data ) ); return $data; } @@ -112,7 +123,8 @@ class WooList_API { * @return array|WP_Error */ public function subscriber_get_by_email( string $email ) { - return $this->call( 'subscriberGetByEmail', [ 'email' => rawurlencode( $email ) ] ); + // No manual encoding — wp_remote_post encodes POST body values correctly. + return $this->call( 'subscriberGetByEmail', [ 'email' => $email ] ); } /** @@ -125,7 +137,7 @@ class WooList_API { return $this->call( 'subscriberAdd', [ - 'email' => rawurlencode( $email ), + 'email' => $email, 'confirmed' => 1, 'htmlemail' => 1, ] @@ -150,9 +162,11 @@ class WooList_API { } /** - * High-level helper: subscribe an email to a list. + * Subscribe an email address to a phpList list. * - * Gets or creates the subscriber, then adds them to the list. + * Looks up the subscriber first; creates one if not found; then adds + * them to the target list. Returns a result array so callers can check + * success without inspecting WP_Error internals. * * @param string $email Subscriber email address. * @param int $list_id phpList list ID.