get_json_params(); $svc = sanitize_key( (string) ( $p['service'] ?? '' ) ); $v = static function ( $k ) use ( $p ) { $x = isset( $p[ $k ] ) ? trim( (string) $p[ $k ] ) : ''; return ( '' === $x || 0 === strpos( $x, '••••' ) ) ? trim( (string) Options::get( $k, '' ) ) : $x; }; switch ( $svc ) { case 'mailchimp': return self::mailchimp( $v( 'mailchimp_key' ) ); case 'recaptcha': return self::captcha( Forms::RECAPTCHA_VERIFY, $v( 'recaptcha_site' ), $v( 'recaptcha_secret' ) ); case 'turnstile': return self::captcha( Forms::TURNSTILE_VERIFY, $v( 'turnstile_site' ), $v( 'turnstile_secret' ) ); case 'instagram': return self::instagram( $v( 'instagram_token' ) ); case 'google_places': return self::places( $v( 'google_places_key' ) ); case 'weather': return self::weather(); } return new \WP_Error( 'akram_bad_service', __( 'Unknown service.', 'akram' ), array( 'status' => 400 ) ); } private static function result( $ok, $message, array $extra = array() ) { return array_merge( array( 'ok' => (bool) $ok, 'message' => $message, ), $extra ); } private static function http_error( $res ) { /* translators: %s: error message from the network layer */ return self::result( false, sprintf( __( 'Could not reach the service: %s', 'akram' ), $res->get_error_message() ) ); } private static function mailchimp( $key ) { if ( '' === $key ) { return self::result( false, __( 'Enter an API key first.', 'akram' ) ); } if ( ! preg_match( '/-([a-z]+[0-9]+)$/', $key, $m ) ) { return self::result( false, __( 'This does not look like a Mailchimp API key. It ends with your data center, for example “-us21”.', 'akram' ) ); } $res = wp_remote_get( "https://{$m[1]}.api.mailchimp.com/3.0/lists?count=100&fields=lists.id,lists.name,lists.stats.member_count", array( 'timeout' => 10, 'headers' => array( 'Authorization' => 'Basic ' . base64_encode( 'akram:' . $key ) ), ) ); if ( is_wp_error( $res ) ) { return self::http_error( $res ); } $body = json_decode( wp_remote_retrieve_body( $res ), true ); if ( 200 !== wp_remote_retrieve_response_code( $res ) ) { /* translators: %s: error from Mailchimp */ return self::result( false, sprintf( __( 'Mailchimp rejected the key: %s', 'akram' ), $body['detail'] ?? wp_remote_retrieve_response_message( $res ) ) ); } $lists = array(); foreach ( (array) ( $body['lists'] ?? array() ) as $l ) { $lists[] = array( 'id' => (string) $l['id'], 'name' => (string) $l['name'], ); } /* translators: %d: number of audiences */ return self::result( true, sprintf( _n( 'Connected. %d audience found — use its ID in the form’s Mailchimp settings.', 'Connected. %d audiences found — use an ID in the form’s Mailchimp settings.', count( $lists ), 'akram' ), count( $lists ) ), array( 'lists' => $lists ) ); } /** reCAPTCHA and Turnstile share the siteverify protocol: a dummy token tells a bad secret apart from a good one. */ private static function captcha( $url, $site, $secret ) { if ( '' === $site || '' === $secret ) { return self::result( false, __( 'Enter both the site key and the secret key.', 'akram' ) ); } $res = wp_remote_post( $url, array( 'timeout' => 8, 'body' => array( 'secret' => $secret, 'response' => 'akram-connection-test', ), ) ); if ( is_wp_error( $res ) ) { return self::http_error( $res ); } $body = json_decode( wp_remote_retrieve_body( $res ), true ); $codes = (array) ( $body['error-codes'] ?? array() ); if ( ! is_array( $body ) || in_array( 'invalid-input-secret', $codes, true ) || in_array( 'missing-input-secret', $codes, true ) ) { return self::result( false, __( 'The secret key was rejected. Copy it again from the provider’s dashboard.', 'akram' ) ); } if ( ! empty( $body['success'] ) ) { return self::result( true, __( 'Secret key accepted. This is a testing key: every visitor passes the check.', 'akram' ) ); } if ( false !== strpos( $url, 'google.com' ) ) { // Google validates the visitor token before the secret, so a bad secret can't be detected without a real visitor. if ( '6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI' === $site ) { return self::result( false, __( 'These are Google’s reCAPTCHA v2 test keys. Akram uses reCAPTCHA v3 — create v3 keys in the reCAPTCHA admin console.', 'akram' ) ); } return self::result( true, __( 'Google is reachable. It only checks the secret key together with a real visitor, so submit a form with CAPTCHA enabled to finish the check.', 'akram' ) ); } return self::result( true, __( 'Secret key accepted.', 'akram' ) ); } private static function instagram( $token ) { if ( '' === $token ) { return self::result( false, __( 'Enter an access token first.', 'akram' ) ); } $res = wp_remote_get( add_query_arg( array( 'fields' => 'id,username', 'access_token' => $token, ), 'https://graph.instagram.com/me' ), array( 'timeout' => 10 ) ); if ( is_wp_error( $res ) ) { return self::http_error( $res ); } $body = json_decode( wp_remote_retrieve_body( $res ), true ); if ( empty( $body['username'] ) ) { /* translators: %s: error from Instagram */ return self::result( false, sprintf( __( 'Instagram rejected the token: %s', 'akram' ), $body['error']['message'] ?? __( 'unknown error', 'akram' ) ) ); } /* translators: %s: Instagram username */ return self::result( true, sprintf( __( 'Connected as @%s.', 'akram' ), $body['username'] ) ); } private static function places( $key ) { if ( '' === $key ) { return self::result( false, __( 'Enter an API key first.', 'akram' ) ); } // A well-known public place: only its name is requested (the cheapest field mask). $res = wp_remote_get( 'https://places.googleapis.com/v1/places/ChIJN1t_tDeuEmsRUsoyG83frY4', array( 'timeout' => 10, 'headers' => array( 'X-Goog-Api-Key' => $key, 'X-Goog-FieldMask' => 'displayName', ), ) ); if ( is_wp_error( $res ) ) { return self::http_error( $res ); } $body = json_decode( wp_remote_retrieve_body( $res ), true ); if ( 200 !== wp_remote_retrieve_response_code( $res ) ) { /* translators: %s: error from Google */ return self::result( false, sprintf( __( 'Google rejected the key: %s', 'akram' ), $body['error']['message'] ?? wp_remote_retrieve_response_message( $res ) ) ); } return self::result( true, __( 'Connected. Places API (New) is enabled for this key.', 'akram' ) ); } private static function weather() { $res = wp_remote_get( 'https://api.open-meteo.com/v1/forecast?latitude=51.51&longitude=-0.13¤t=temperature_2m', array( 'timeout' => 8 ) ); if ( is_wp_error( $res ) ) { return self::http_error( $res ); } $body = json_decode( wp_remote_retrieve_body( $res ), true ); if ( ! isset( $body['current']['temperature_2m'] ) ) { return self::result( false, __( 'The weather service answered unexpectedly.', 'akram' ) ); } /* translators: %s: temperature */ return self::result( true, sprintf( __( 'Reachable (London now: %s °C). The Weather widget needs no key.', 'akram' ), $body['current']['temperature_2m'] ) ); } }