Error message extraction:
- phpList v3 nests the actual message inside data.message:
{"status":"error","data":{"code":0,"message":"invalid call"}}
Previous code read data as a raw value and got "Array" in the logs.
Now checks data.message first, falls back to top-level fields.
listSubscriberAdd:
- Send both naming conventions (listid/subscriberid AND list_id/subscriber_id)
so the call works regardless of which phpList REST API build is installed.
- Add debug log line showing exact param values sent, making future
diagnosis straightforward without needing server-side inspection.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
245 lines
8.4 KiB
PHP
245 lines
8.4 KiB
PHP
<?php
|
|
/**
|
|
* 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
|
|
*/
|
|
|
|
defined( 'ABSPATH' ) || exit;
|
|
|
|
class WooList_API {
|
|
|
|
/**
|
|
* Retrieve a saved plugin option.
|
|
*
|
|
* @param string $key Option key without the woolist_ prefix.
|
|
* @param mixed $default Default value.
|
|
* @return mixed
|
|
*/
|
|
public function get_option( string $key, $default = '' ) {
|
|
return get_option( 'woolist_' . $key, $default );
|
|
}
|
|
|
|
/**
|
|
* Return the base API endpoint URL (no credentials, no command).
|
|
*
|
|
* @return string|WP_Error
|
|
*/
|
|
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' ) );
|
|
}
|
|
|
|
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(
|
|
[
|
|
'login' => (string) $this->get_option( 'phplist_login' ),
|
|
'password' => (string) $this->get_option( 'phplist_password' ),
|
|
'cmd' => $cmd,
|
|
],
|
|
$extra
|
|
);
|
|
|
|
// 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 ) );
|
|
|
|
$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() );
|
|
return $response;
|
|
}
|
|
|
|
$code = wp_remote_retrieve_response_code( $response );
|
|
$body_raw = wp_remote_retrieve_body( $response );
|
|
|
|
// 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( '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_raw, true );
|
|
|
|
if ( json_last_error() !== JSON_ERROR_NONE ) {
|
|
// 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 signals errors via status=error.
|
|
// v3 nests the message inside data.message: {"status":"error","data":{"code":0,"message":"..."}}
|
|
if ( isset( $data['status'] ) && strtolower( $data['status'] ) === 'error' ) {
|
|
if ( is_array( $data['data'] ?? null ) ) {
|
|
$message = $data['data']['message'] ?? $data['errormessage'] ?? $data['message'] ?? 'Unknown API error';
|
|
} else {
|
|
$message = $data['errormessage'] ?? $data['message'] ?? $data['data'] ?? 'Unknown API error';
|
|
}
|
|
$message = (string) $message;
|
|
WooList_Logger::error( 'API error cmd=' . $cmd . ' message=' . $message . ' full_response=' . wp_json_encode( $data ) );
|
|
return new WP_Error( 'woolist_api_error', $message );
|
|
}
|
|
|
|
WooList_Logger::debug( 'API call succeeded cmd=' . $cmd . ' response=' . wp_json_encode( $data ) );
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Retrieve a subscriber by email address.
|
|
*
|
|
* @param string $email Subscriber email.
|
|
* @return array|WP_Error
|
|
*/
|
|
public function subscriber_get_by_email( string $email ) {
|
|
// No manual encoding — wp_remote_post encodes POST body values correctly.
|
|
return $this->call( 'subscriberGetByEmail', [ 'email' => $email ] );
|
|
}
|
|
|
|
/**
|
|
* Add a new confirmed subscriber.
|
|
*
|
|
* @param string $email Subscriber email.
|
|
* @return array|WP_Error
|
|
*/
|
|
public function subscriber_add( string $email ) {
|
|
return $this->call(
|
|
'subscriberAdd',
|
|
[
|
|
'email' => $email,
|
|
'confirmed' => 1,
|
|
'htmlemail' => 1,
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Add a subscriber (by ID) to a phpList list.
|
|
*
|
|
* phpList REST API v3 accepts both "listid"/"subscriberid" (older) and
|
|
* "list_id"/"subscriber_id" (some builds). We send all four so whichever
|
|
* naming convention this installation uses will be satisfied.
|
|
*
|
|
* @param int $list_id phpList list ID.
|
|
* @param int $subscriber_id phpList subscriber ID.
|
|
* @return array|WP_Error
|
|
*/
|
|
public function list_subscriber_add( int $list_id, int $subscriber_id ) {
|
|
WooList_Logger::debug(
|
|
'listSubscriberAdd params listid=' . $list_id . ' subscriberid=' . $subscriber_id
|
|
);
|
|
return $this->call(
|
|
'listSubscriberAdd',
|
|
[
|
|
'listid' => $list_id,
|
|
'subscriberid' => $subscriber_id,
|
|
'list_id' => $list_id, // alternate param name used by some v3 builds
|
|
'subscriber_id' => $subscriber_id, // alternate param name
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Subscribe an email address to a phpList 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.
|
|
* @return array{success: bool, subscriber_id: int|null}
|
|
*/
|
|
public function subscribe_email_to_list( string $email, int $list_id ): array {
|
|
WooList_Logger::debug( 'subscribe_email_to_list email=' . $email . ' list_id=' . $list_id );
|
|
|
|
// Step 1: look up existing subscriber.
|
|
$subscriber_id = null;
|
|
$existing = $this->subscriber_get_by_email( $email );
|
|
|
|
// phpList wraps subscriber data inside a "data" key: {"status":"success","data":{"id":…}}
|
|
$existing_id = $existing['data']['id'] ?? $existing['id'] ?? null;
|
|
if ( ! is_wp_error( $existing ) && ! empty( $existing_id ) ) {
|
|
$subscriber_id = (int) $existing_id;
|
|
WooList_Logger::debug( 'Found existing subscriber id=' . $subscriber_id . ' email=' . $email );
|
|
} else {
|
|
// Step 2: create a new subscriber.
|
|
WooList_Logger::debug( 'Subscriber not found, creating new email=' . $email );
|
|
$added = $this->subscriber_add( $email );
|
|
|
|
if ( is_wp_error( $added ) ) {
|
|
WooList_Logger::error( 'Could not create subscriber email=' . $email . ' error=' . $added->get_error_message() );
|
|
return [ 'success' => false, 'subscriber_id' => null ];
|
|
}
|
|
|
|
// phpList wraps the new subscriber inside a "data" key.
|
|
$subscriber_id = isset( $added['data']['id'] ) ? (int) $added['data']['id']
|
|
: ( isset( $added['id'] ) ? (int) $added['id'] : null );
|
|
|
|
if ( $subscriber_id ) {
|
|
WooList_Logger::info( 'Created new subscriber id=' . $subscriber_id . ' email=' . $email );
|
|
} else {
|
|
WooList_Logger::error( 'API returned no subscriber ID after add email=' . $email . ' response=' . wp_json_encode( $added ) );
|
|
return [ 'success' => false, 'subscriber_id' => null ];
|
|
}
|
|
}
|
|
|
|
// Step 3: add subscriber to the list.
|
|
WooList_Logger::debug( 'Adding subscriber ' . $subscriber_id . ' to list ' . $list_id );
|
|
$result = $this->list_subscriber_add( $list_id, $subscriber_id );
|
|
|
|
if ( is_wp_error( $result ) ) {
|
|
WooList_Logger::error( 'Could not add subscriber to list subscriber_id=' . $subscriber_id . ' list_id=' . $list_id . ' error=' . $result->get_error_message() );
|
|
return [ 'success' => false, 'subscriber_id' => $subscriber_id ];
|
|
}
|
|
|
|
WooList_Logger::info( 'Subscribed email=' . $email . ' subscriber_id=' . $subscriber_id . ' list_id=' . $list_id );
|
|
return [ 'success' => true, 'subscriber_id' => $subscriber_id ];
|
|
}
|
|
|
|
/**
|
|
* Retrieve all lists (used for connection testing).
|
|
*
|
|
* @return array|WP_Error
|
|
*/
|
|
public function lists_get() {
|
|
return $this->call( 'listsGet' );
|
|
}
|
|
}
|