rawurlencode( home_url( '/' ) ), 'state' => rawurlencode( $state ), 'return' => rawurlencode( admin_url( 'options-general.php?page=flatratechat' ) ), ), self::base_url() . '/connect-wordpress' ); } /** * Exchange the one-time connect code for the site key and the read token; store them. * * @param string $code Signed one-time code from the service. * @return true|WP_Error */ public static function exchange_code( $code ) { $code = trim( (string) $code ); if ( ! preg_match( '/^[A-Za-z0-9_\-.]{20,2000}$/', $code ) ) { return new WP_Error( 'frc_bad_code', __( 'That connection code does not look right. Click Connect again, or copy the whole code.', 'flatratechat' ) ); } $res = self::request( 'POST', '/api/wp/exchange', array( 'code' => $code ) ); if ( is_wp_error( $res ) ) { return $res; } if ( empty( $res['ok'] ) || empty( $res['key'] ) || empty( $res['token'] ) || ! preg_match( '/^(pk_|wk)[A-Za-z0-9]{10,40}$/', (string) $res['key'] ) ) { return new WP_Error( 'frc_bad_response', __( 'FlatRateChat did not return a site key. Click Connect again.', 'flatratechat' ) ); } update_option( self::OPT_KEY, sanitize_text_field( (string) $res['key'] ), false ); update_option( self::OPT_TOKEN, sanitize_text_field( (string) $res['token'] ), false ); update_option( self::OPT_HOST, sanitize_text_field( isset( $res['host'] ) ? (string) $res['host'] : '' ), false ); update_option( self::OPT_CONNECTED, time(), false ); delete_transient( self::TR_STATUS ); if ( isset( $res['status'] ) && is_array( $res['status'] ) ) { set_transient( self::TR_STATUS, $res['status'], 5 * MINUTE_IN_SECONDS ); } return true; } /** * Plan + usage for the settings page (read with the site token; cached 5 minutes). * * @param bool $force Skip the cache. * @return array|WP_Error */ public static function status( $force = false ) { if ( ! self::is_connected() ) { return new WP_Error( 'frc_not_connected', __( 'Not connected.', 'flatratechat' ) ); } if ( ! $force ) { $cached = get_transient( self::TR_STATUS ); if ( is_array( $cached ) ) { return $cached; } } $res = self::request( 'GET', '/api/wp/status', null, (string) get_option( self::OPT_TOKEN, '' ) ); if ( is_wp_error( $res ) ) { if ( self::is_stale_error( $res ) ) { // The service no longer accepts this connection (token revoked or replaced, or the site was deleted // in the dashboard): stop loading the bubble and ask for a new Connect. The host is kept for the notice. self::mark_stale(); } return $res; } if ( empty( $res['ok'] ) ) { return new WP_Error( 'frc_status', isset( $res['message'] ) ? (string) $res['message'] : __( 'FlatRateChat could not return the status of this site.', 'flatratechat' ) ); } if ( ! empty( $res['token'] ) && is_string( $res['token'] ) ) { // The service refreshes the status token when it is in its last months; keep the new one, never cache it. update_option( self::OPT_TOKEN, sanitize_text_field( $res['token'] ), false ); unset( $res['token'] ); } set_transient( self::TR_STATUS, $res, 5 * MINUTE_IN_SECONDS ); return $res; } /** * Whether a status error means the connection itself is gone (401 revoked/unauthorized, 404 no_site), as opposed * to a temporary problem (network, 5xx, rate limit). * * @param WP_Error $err Error from request(). * @return bool */ public static function is_stale_error( $err ) { $data = $err->get_error_data(); if ( ! is_array( $data ) ) { return false; } $code = isset( $data['error'] ) ? (string) $data['error'] : ''; $status = isset( $data['status'] ) ? (int) $data['status'] : 0; return ( 401 === $status && in_array( $code, array( 'revoked', 'unauthorized' ), true ) ) || ( 404 === $status && 'no_site' === $code ); } /** * Drop the key and token (the bubble stops loading, the settings page shows the Connect button) but keep the host * and connection time, so the page can say "Disconnected, please Connect again". */ public static function mark_stale() { delete_option( self::OPT_KEY ); delete_option( self::OPT_TOKEN ); delete_transient( self::TR_STATUS ); } /** * A host is remembered but no key/token: the service ended the connection (see mark_stale()). * * @return bool */ public static function is_stale() { return ! self::is_connected() && '' !== (string) get_option( self::OPT_HOST, '' ); } /** * Forget the connection on this site. The FlatRateChat account itself is managed at flatratechat.com. * * Tells the service to invalidate the status token first (best effort: a failed call never blocks the local * clean-up; connecting again issues a new token anyway). */ public static function disconnect() { $token = (string) get_option( self::OPT_TOKEN, '' ); if ( '' !== $token ) { self::request( 'POST', '/api/wp/revoke', array(), $token ); } delete_option( self::OPT_KEY ); delete_option( self::OPT_TOKEN ); delete_option( self::OPT_HOST ); delete_option( self::OPT_CONNECTED ); delete_transient( self::TR_STATUS ); delete_transient( self::TR_STATE ); } /** * One HTTP call to the service. * * @param string $method GET or POST. * @param string $path Path starting with /api/. * @param array|null $body JSON body for POST. * @param string|null $token Bearer token for authenticated reads. * @return array|WP_Error Decoded JSON. */ private static function request( $method, $path, $body = null, $token = null ) { $args = array( 'method' => $method, 'timeout' => 15, 'headers' => array( 'accept' => 'application/json', 'user-agent' => 'FlatRateChat-WordPress/' . FLATRATECHAT_VERSION . '; ' . home_url( '/' ), ), ); if ( null !== $body ) { $args['headers']['content-type'] = 'application/json'; $args['body'] = wp_json_encode( $body ); } if ( $token ) { $args['headers']['authorization'] = 'Bearer ' . $token; } $res = wp_remote_request( self::base_url() . $path, $args ); if ( is_wp_error( $res ) ) { return new WP_Error( 'frc_http', __( 'Could not reach FlatRateChat. Please try again in a minute.', 'flatratechat' ) ); } $data = json_decode( (string) wp_remote_retrieve_body( $res ), true ); if ( ! is_array( $data ) ) { return new WP_Error( 'frc_http', __( 'FlatRateChat returned an unexpected answer. Please try again in a minute.', 'flatratechat' ) ); } if ( wp_remote_retrieve_response_code( $res ) >= 400 && empty( $data['ok'] ) ) { $msg = isset( $data['message'] ) ? (string) $data['message'] : __( 'FlatRateChat refused the request.', 'flatratechat' ); return new WP_Error( 'frc_api', $msg, array( 'error' => isset( $data['error'] ) ? sanitize_key( (string) $data['error'] ) : '', 'status' => (int) wp_remote_retrieve_response_code( $res ), ) ); } return $data; } }